From 716ff96b075c2b8dd741087a85caf11bf144d24a Mon Sep 17 00:00:00 2001 From: skycurtain Date: Wed, 7 Oct 2026 19:54:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E7=94=A8=E6=88=B7=E4=B8=8B?= =?UTF-8?q?=E6=8B=89=E8=8F=9C=E5=8D=95=E6=94=B9=E4=B8=BA=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE/=E9=80=80=E5=87=BA=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BC=9A=E8=AF=9D=E7=94=A8=E6=88=B7=E5=90=8D=E7=A9=BA?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/server/src/routes/admin-session.ts | 8 +++ apps/web/src/layouts/admin-layout.vue | 86 ++++++++++++++++++++----- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/apps/server/src/routes/admin-session.ts b/apps/server/src/routes/admin-session.ts index ea9edb4..a927097 100644 --- a/apps/server/src/routes/admin-session.ts +++ b/apps/server/src/routes/admin-session.ts @@ -51,6 +51,14 @@ const DUMMY_PASSWORD_HASH = export function adminSessionRoutes() { const r = new OpenAPIHono<{ Variables: AppVars }>(); + // 只保护需要会话的端点:GET/DELETE /session、PUT /password + // POST /session 是登录、setup* 是引导——不能套鉴权 + r.use("/session", async (c, next) => { + if (c.req.method === "POST") return next(); + return requireAdminSession(c, next); + }); + r.use("/password", requireAdminSession); + r.openapi( createRoute({ method: "get", diff --git a/apps/web/src/layouts/admin-layout.vue b/apps/web/src/layouts/admin-layout.vue index ffc7bb6..3e5178f 100644 --- a/apps/web/src/layouts/admin-layout.vue +++ b/apps/web/src/layouts/admin-layout.vue @@ -5,11 +5,14 @@ import { AccountBookOutlined, AuditOutlined, DashboardOutlined, + DownOutlined, KeyOutlined, + LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, SettingOutlined, TeamOutlined, + UserOutlined, } from "@antdv-next/icons"; import { computed, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; @@ -20,7 +23,12 @@ const { token } = theme.useToken(); const route = useRoute(); const router = useRouter(); const adminSession = useAdminSessionStore(); -const sessionQuery = useSession(); +// 必须解构:未解构时经 reactive 访问会解包 Ref,再取 .value 会得到 undefined +const { + data: sessionData, + isPending: sessionPending, + isFetching: sessionFetching, +} = useSession(); const logout = useLogout(); const collapsed = ref(false); @@ -28,7 +36,7 @@ const collapsed = ref(false); const brandTitle = "NAKU"; const brandDescription = "NewAPI 用量统计"; const username = computed( - () => adminSession.username || sessionQuery.data.value?.username || "", + () => adminSession.username || sessionData.value?.username || "", ); /** 收起时仅剩图标——每项必须带 icon(交互共性侧边栏六项) */ @@ -53,9 +61,21 @@ function onMenuClick({ key }: MenuInfo) { void router.push(String(key)); } -async function onLogout() { - await logout.mutateAsync().catch(() => undefined); - void router.push({ name: "admin-login" }); +const userMenuItems: MenuItemType[] = [ + { key: "settings", icon: UserOutlined, label: "账号设置" }, + { type: "divider", key: "d1" }, + { key: "logout", icon: LogoutOutlined, label: "退出", danger: true }, +]; + +async function onUserMenuClick({ key }: MenuInfo) { + if (key === "settings") { + void router.push({ name: "admin-settings" }); + return; + } + if (key === "logout") { + await logout.mutateAsync().catch(() => undefined); + void router.push({ name: "admin-login" }); + } } @@ -96,12 +116,21 @@ async function onLogout() { - + + +