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.
272 lines
8.9 KiB
Vue
272 lines
8.9 KiB
Vue
<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>
|