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:
271
app/error.vue
Normal file
271
app/error.vue
Normal 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>
|
||||
Reference in New Issue
Block a user