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

271
app/error.vue Normal file
View File

@@ -0,0 +1,271 @@
<script setup lang="ts">
import type { NuxtError } from "#app";
// 定义扩展错误类型
interface ExtendedError extends NuxtError {
url?: string;
}
const props = defineProps<{ error?: ExtendedError }>();
const router = useRouter();
const route = useRoute();
const status = computed(() => props.error?.statusCode ?? 500);
const statusMessage = computed(() => props.error?.statusMessage ?? "");
const title = computed(() => {
switch (status.value) {
case 404:
return "页面未找到";
case 403:
return "禁止访问";
case 500:
return "服务器内部错误";
default:
return `${status.value} 错误`;
}
});
useHead({ title: title.value });
const friendlyMessage = computed(() => {
if (status.value === 404)
return "抱歉,我们找不到您要访问的页面。它可能已被移动、删除或从未存在过。";
if (status.value === 403)
return "抱歉,您没有权限访问此页面。请检查您的账户权限或联系管理员。";
if (status.value === 500)
return "服务器出了点问题,我们正在努力修复。请稍后再试。";
return props.error?.message ?? "发生了未知错误,我们的技术团队已经收到通知。";
});
const goHome = () => router.push("/");
const goBack = () => {
if (import.meta.client && window.history.length > 1) router.back();
else router.push("/");
};
const reloadPage = () => {
if (import.meta.client) window.location.reload();
};
const contactSupport = () => {
if (!import.meta.client) return;
const subject = encodeURIComponent(
`网站错误反馈 — ${title.value}${route.fullPath}`
);
const body = encodeURIComponent(`错误信息:
Status: ${status.value} ${statusMessage.value}
Path: ${route.fullPath}
请描述您在做什么以及复现步骤:`);
// 请替换为真实的支持邮箱或工单链接
window.location.href = `mailto:hello@example.com?subject=${subject}&body=${body}`;
};
const isDev = import.meta.dev;
// 格式化错误信息
const formattedDebugInfo = computed(() => {
if (!props.error) return isDev ? "无错误对象传入。" : "错误详情已隐藏。";
if (isDev) {
try {
return {
status: props.error.statusCode,
message: props.error.message || props.error.statusMessage,
url: props.error.url,
stack: props.error.stack,
};
} catch (e) {
return { error: String(props.error) };
}
}
return {
status: props.error.statusCode || "N/A",
message: props.error.message || props.error.statusMessage || "N/A",
};
});
// 错误图标
const errorIcon = computed(() => {
switch (status.value) {
case 404:
return "mdi:magnify-close";
case 403:
return "mdi:lock-alert";
case 500:
return "mdi:server-off";
default:
return "mdi:alert-circle";
}
});
// 错误颜色
const errorColor = computed(() => {
switch (status.value) {
case 404:
return "#fb9e3a"; // 使用主题色
case 403:
return "#e74c3c";
case 500:
return "#c0392b";
default:
return "#fb9e3a"; // 使用主题色
}
});
</script>
<template>
<main
class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4"
>
<div
class="max-w-3xl w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg overflow-hidden"
>
<!-- 顶部状态栏 -->
<div class="h-2 w-full" :style="{ backgroundColor: errorColor }"></div>
<div class="p-8 md:p-10">
<div class="flex flex-col md:flex-row items-center gap-8">
<!-- 错误代码和图标 -->
<div class="flex-none text-center">
<div class="relative">
<div
class="w-32 h-32 rounded-full flex items-center justify-center mx-auto mb-4"
:style="{ backgroundColor: errorColor + '20' }"
>
<div class="text-4xl" :style="{ color: errorColor }">
<span v-if="status === 404">404</span>
<span v-else-if="status === 403">403</span>
<span v-else-if="status === 500">500</span>
<span v-else>{{ status }}</span>
</div>
</div>
</div>
<div
class="text-sm text-gray-500 dark:text-gray-400 font-medium mt-2"
>
{{ statusMessage || title }}
</div>
</div>
<!-- 错误信息和操作 -->
<div class="flex-1">
<h1
class="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-3"
>
{{ title }}
</h1>
<p class="text-gray-600 dark:text-gray-300 leading-relaxed mb-6">
{{ friendlyMessage }}
</p>
<!-- 操作按钮组 -->
<div class="flex flex-wrap gap-3 mb-6">
<button
@click="goHome"
class="px-5 py-2.5 rounded-md text-white font-medium transition-colors flex items-center gap-2"
style="background-color: var(--color-primary)"
>
<span>返回首页</span>
</button>
<button
@click="goBack"
class="px-5 py-2.5 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
返回上页
</button>
<button
@click="reloadPage"
class="px-5 py-2.5 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
重新加载
</button>
</div>
<!-- 报告问题按钮 -->
<div class="mb-6">
<button
@click="contactSupport"
class="px-5 py-2.5 rounded-md bg-gray-800 dark:bg-gray-700 text-white font-medium hover:bg-gray-700 dark:hover:bg-gray-600 transition-colors flex items-center gap-2"
>
<span>报告问题</span>
</button>
</div>
<!-- 提示信息 -->
<div class="text-sm text-gray-500 dark:text-gray-400 mb-6">
如果这是由链接或书签导致的请尝试返回或访问主页
</div>
<!-- 错误详情 -->
<details
class="border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden"
>
<summary
class="cursor-pointer p-3 bg-gray-50 dark:bg-gray-900 font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
>
错误详情{{ isDev ? "开发模式" : "精简信息" }}
</summary>
<div
class="p-4 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 overflow-auto max-h-64"
>
<div v-if="typeof formattedDebugInfo === 'object'">
<div v-if="isDev" class="space-y-4">
<div v-if="formattedDebugInfo.status">
<strong>状态码:</strong> {{ formattedDebugInfo.status }}
</div>
<div v-if="formattedDebugInfo.message">
<strong>消息:</strong> {{ formattedDebugInfo.message }}
</div>
<div v-if="formattedDebugInfo.url">
<strong>URL:</strong> {{ formattedDebugInfo.url }}
</div>
<div v-if="formattedDebugInfo.stack" class="mt-3">
<strong>堆栈跟踪:</strong>
<pre
class="mt-2 text-xs bg-gray-50 dark:bg-gray-900 p-3 rounded overflow-x-auto"
>{{ formattedDebugInfo.stack }}</pre
>
</div>
</div>
<div v-else>
<div v-if="formattedDebugInfo.status">
<strong>状态码:</strong> {{ formattedDebugInfo.status }}
</div>
<div v-if="formattedDebugInfo.message">
<strong>消息:</strong> {{ formattedDebugInfo.message }}
</div>
</div>
</div>
<div v-else>
{{ formattedDebugInfo }}
</div>
</div>
</details>
</div>
</div>
</div>
</div>
</main>
</template>
<style scoped>
/* 自定义样式补充 */
details[open] summary {
border-bottom: 1px solid #e5e7eb;
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
details[open] summary {
border-bottom-color: #4b5563;
}
}
/* 响应式调整 */
@media (max-width: 640px) {
.text-4xl {
font-size: 2rem;
}
}
</style>