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:
xiaomai
2025-10-22 21:40:30 +08:00
parent 1fedf7094c
commit e7f2bc2c47
11 changed files with 3902 additions and 3 deletions

View 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>