Browse Source
Introduce a new error handling component to display user-friendly error messages based on HTTP status codes. Implement global configuration management to control registration availability across the application. Update various components and pages to utilize the new configuration, ensuring consistent behavior for registration links and error handling. Enhance middleware to bypass authentication for specific paths and improve overall user experience.feat/auth-access-control
14 changed files with 279 additions and 87 deletions
@ -0,0 +1,36 @@ |
|||
import { request, unwrapApiBody, type ApiResponse } from '../utils/http/factory' |
|||
|
|||
export type GlobalConfig = { |
|||
allowRegister: boolean |
|||
} |
|||
|
|||
type GlobalConfigResult = { |
|||
config: GlobalConfig |
|||
} |
|||
|
|||
const DEFAULT_GLOBAL_CONFIG: GlobalConfig = { |
|||
allowRegister: true, |
|||
} |
|||
|
|||
export function useGlobalConfig() { |
|||
const { data, pending, error, refresh } = useAsyncData( |
|||
'global:config', |
|||
async () => { |
|||
const payload = await request<ApiResponse<GlobalConfigResult>>('/api/config/global') |
|||
return unwrapApiBody(payload).config |
|||
}, |
|||
{ |
|||
default: () => ({ ...DEFAULT_GLOBAL_CONFIG }), |
|||
}, |
|||
) |
|||
|
|||
const config = computed<GlobalConfig>(() => data.value ?? DEFAULT_GLOBAL_CONFIG) |
|||
|
|||
return { |
|||
config, |
|||
allowRegister: computed(() => config.value.allowRegister), |
|||
pending, |
|||
error, |
|||
refresh, |
|||
} |
|||
} |
|||
@ -0,0 +1,126 @@ |
|||
<script setup lang="ts"> |
|||
import type { NuxtError } from "#app"; |
|||
|
|||
const props = defineProps<{ |
|||
error: NuxtError; |
|||
}>(); |
|||
|
|||
const statusCode = computed(() => props.error?.statusCode ?? 500); |
|||
const statusMessage = computed(() => { |
|||
if (props.error?.statusMessage) { |
|||
return props.error.statusMessage; |
|||
} |
|||
return statusCode.value >= 500 ? "服务暂时不可用,请稍后重试" : "请求失败,请检查后重试"; |
|||
}); |
|||
const errorTitle = computed(() => { |
|||
if (statusCode.value === 401) return "未登录或会话已失效"; |
|||
if (statusCode.value === 403) return "当前账号无访问权限"; |
|||
if (statusCode.value === 404) return "页面不存在"; |
|||
if (statusCode.value >= 500) return "服务开小差了"; |
|||
return "发生了一点异常"; |
|||
}); |
|||
const tips = computed(() => { |
|||
if (statusCode.value === 401) return "请重新登录后再继续操作。"; |
|||
if (statusCode.value === 403) return "如需访问此页面,请联系管理员开通权限。"; |
|||
if (statusCode.value === 404) return "你访问的地址可能已变更或被移除。"; |
|||
if (statusCode.value >= 500) return "我们正在处理该问题,请稍后再试。"; |
|||
return "你可以先返回首页,或点击重试。"; |
|||
}); |
|||
const levelTag = computed(() => { |
|||
if (statusCode.value >= 500) return "P1 / 高优先级"; |
|||
if (statusCode.value === 401 || statusCode.value === 403) return "P2 / 权限类"; |
|||
if (statusCode.value === 404) return "P3 / 资源类"; |
|||
return "P3 / 一般异常"; |
|||
}); |
|||
const actionHints = computed(() => { |
|||
if (statusCode.value === 401) { |
|||
return ["确认登录状态是否过期", "重新登录后刷新页面", "检查接口鉴权中间件配置"]; |
|||
} |
|||
if (statusCode.value === 403) { |
|||
return ["确认账号角色与权限策略", "检查后端权限拦截规则", "联系管理员开通访问权限"]; |
|||
} |
|||
if (statusCode.value === 404) { |
|||
return ["确认访问路径是否正确", "检查路由或接口是否已下线", "核对部署环境与 baseURL 配置"]; |
|||
} |
|||
if (statusCode.value >= 500) { |
|||
return ["查看服务端日志定位堆栈", "检查依赖服务和数据库连接", "确认最新变更是否引入回归"]; |
|||
} |
|||
return ["查看浏览器控制台错误信息", "检查网络请求返回内容", "重试并记录复现路径"]; |
|||
}); |
|||
|
|||
const handleBackHome = () => clearError({ redirect: "/" }); |
|||
const handleRetry = async () => { |
|||
if (statusCode.value === 401 || statusCode.value === 403) { |
|||
await clearError({ redirect: "/login" }); |
|||
return; |
|||
} |
|||
|
|||
if (import.meta.client) { |
|||
window.location.reload(); |
|||
return; |
|||
} |
|||
|
|||
await clearError({ redirect: "/" }); |
|||
}; |
|||
</script> |
|||
|
|||
<template> |
|||
<main class="min-h-screen bg-default text-default"> |
|||
<section class="mx-auto w-full max-w-5xl px-4 py-10 sm:px-6"> |
|||
<header class="mb-4 flex items-center justify-between rounded-xl border border-default bg-elevated px-4 py-3"> |
|||
<div class="flex items-center gap-2"> |
|||
<span class="size-2 rounded-full bg-error" /> |
|||
<p class="text-sm font-semibold text-toned">运行异常面板</p> |
|||
</div> |
|||
<span class="rounded-md border border-default px-2 py-1 text-xs text-muted"> |
|||
{{ levelTag }} |
|||
</span> |
|||
</header> |
|||
|
|||
<div class="grid gap-4 lg:grid-cols-[1.35fr,1fr]"> |
|||
<article class="rounded-xl border border-default bg-elevated p-6 sm:p-7"> |
|||
<p class="mb-1 text-xs tracking-wide text-muted">状态码</p> |
|||
<p class="mb-3 font-mono text-5xl font-semibold leading-none text-error sm:text-6xl"> |
|||
{{ statusCode }} |
|||
</p> |
|||
<h1 class="mb-2 text-3xl font-semibold tracking-tight"> |
|||
{{ errorTitle }} |
|||
</h1> |
|||
<p class="mb-2 text-toned"> |
|||
{{ statusMessage }} |
|||
</p> |
|||
<p class="mb-6 text-sm text-muted"> |
|||
{{ tips }} |
|||
</p> |
|||
|
|||
<div class="flex flex-wrap gap-3"> |
|||
<UButton color="error" variant="solid" size="md" @click="handleRetry"> |
|||
立即重试 |
|||
</UButton> |
|||
<UButton color="neutral" variant="outline" size="md" @click="handleBackHome"> |
|||
返回首页 |
|||
</UButton> |
|||
</div> |
|||
</article> |
|||
|
|||
<aside class="rounded-xl border border-default bg-elevated p-6 sm:p-7"> |
|||
<p class="mb-3 text-xs tracking-wide text-muted">建议排查项</p> |
|||
<ul class="mb-5 space-y-2 text-sm text-toned"> |
|||
<li v-for="hint in actionHints" :key="hint" class="rounded-lg border border-default bg-default px-3 py-2"> |
|||
{{ hint }} |
|||
</li> |
|||
</ul> |
|||
|
|||
<details v-if="error?.message" class="rounded-lg border border-default bg-default p-3 mt-2"> |
|||
<summary class="cursor-pointer select-none text-sm font-medium text-toned"> |
|||
查看错误详情 |
|||
</summary> |
|||
<p class="mt-2 break-words text-xs text-muted"> |
|||
{{ error.message }} |
|||
</p> |
|||
</details> |
|||
</aside> |
|||
</div> |
|||
</section> |
|||
</main> |
|||
</template> |
|||
Loading…
Reference in new issue