feat(admin): implement initial admin dashboard and member management
This commit introduces the foundational structure for the admin dashboard, focusing on the member management feature. Key additions include: - A new page at `/admin/manage/members` to display and manage members. - An `AddModal` component with a comprehensive form for creating new members, featuring validation with Zod and input masking. - A reusable `PhoneInput` component with country code selection, backed by a new `useCountries` composable and a full country dataset. - A custom, user-friendly global error page (`error.vue`) to handle application errors gracefully. - Updated dashboard sidebar navigation to include the new member management section. - Added recommended VS Code extensions and settings to improve developer experience.
This commit is contained in:
165
app/pages/admin/manage/members/index.vue
Normal file
165
app/pages/admin/manage/members/index.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar :title="pageTitle" toggle>
|
||||
<template #leading>
|
||||
<UDashboardSidebarCollapse />
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<AdminManageMembersAddModal />
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
</template>
|
||||
<template #body>
|
||||
<UTable
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:loading="status === 'pending'"
|
||||
class="flex-1 h-80"
|
||||
/>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TableColumn } from "@nuxt/ui";
|
||||
import type { Row } from '@tanstack/vue-table'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
|
||||
const pageTitle = "会员籍管理";
|
||||
definePageMeta({
|
||||
layout: "admin-dashboard",
|
||||
title: pageTitle,
|
||||
});
|
||||
useHead({
|
||||
title: pageTitle,
|
||||
});
|
||||
|
||||
const UAvatar = resolveComponent("UAvatar");
|
||||
const UButton = resolveComponent("UButton");
|
||||
const UBadge = resolveComponent("UBadge");
|
||||
const UDropdownMenu = resolveComponent("UDropdownMenu");
|
||||
|
||||
const toast = useToast()
|
||||
const { copy } = useClipboard()
|
||||
|
||||
type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
avatar: { src: string };
|
||||
company: { name: string };
|
||||
};
|
||||
|
||||
const { data, status } = await useFetch<User[]>(
|
||||
"https://jsonplaceholder.typicode.com/users",
|
||||
{
|
||||
key: "table-users",
|
||||
transform: (data) => {
|
||||
return (
|
||||
data?.map((user) => ({
|
||||
...user,
|
||||
avatar: {
|
||||
src: `https://i.pravatar.cc/120?img=${user.id}`,
|
||||
alt: `${user.name} avatar`,
|
||||
},
|
||||
})) || []
|
||||
);
|
||||
},
|
||||
lazy: true,
|
||||
}
|
||||
);
|
||||
|
||||
const columns: TableColumn<User>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
cell: ({ row }) => {
|
||||
return h("div", { class: "flex items-center gap-3" }, [
|
||||
h(UAvatar, {
|
||||
...row.original.avatar,
|
||||
size: "lg",
|
||||
}),
|
||||
h("div", undefined, [
|
||||
h("p", { class: "font-medium text-highlighted" }, row.original.name),
|
||||
h("p", { class: "" }, `@${row.original.username}`),
|
||||
]),
|
||||
]);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: "Email",
|
||||
},
|
||||
{
|
||||
accessorKey: "company",
|
||||
header: "Company",
|
||||
cell: ({ row }) => row.original.company.name,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return h(
|
||||
"div",
|
||||
{ class: "text-right" },
|
||||
h(
|
||||
UDropdownMenu,
|
||||
{
|
||||
content: {
|
||||
align: "end",
|
||||
},
|
||||
items: getRowItems(row),
|
||||
"aria-label": "Actions dropdown",
|
||||
},
|
||||
() =>
|
||||
h(UButton, {
|
||||
icon: "i-lucide-ellipsis-vertical",
|
||||
color: "neutral",
|
||||
variant: "ghost",
|
||||
class: "ml-auto",
|
||||
"aria-label": "Actions dropdown",
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function getRowItems(row: Row<object>) {
|
||||
return [
|
||||
{
|
||||
type: 'label',
|
||||
label: 'Actions'
|
||||
},
|
||||
{
|
||||
label: 'Copy payment ID',
|
||||
onSelect() {
|
||||
copy(row.original.id)
|
||||
|
||||
toast.add({
|
||||
title: 'Payment ID copied to clipboard!',
|
||||
color: 'success',
|
||||
icon: 'i-lucide-circle-check'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'View customer'
|
||||
},
|
||||
{
|
||||
label: 'View payment details'
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
Reference in New Issue
Block a user