feat(web): 前端逻辑骨架——http/router/stores/queries 与薄壳路由

This commit is contained in:
2026-10-07 14:36:06 +08:00
parent 9ecf817df2
commit fbb989c3fb
39 changed files with 1431 additions and 175 deletions
+56
View File
@@ -0,0 +1,56 @@
import type { RouteRecordRaw } from "vue-router";
import AdminLayout from "@/layouts/admin-layout.vue";
/**
* 管理面路由 `/admin/*`(契约:页面路由永不进入 `/api`)。
* 侧边栏六项 + 账号设置(交互共性 §2)。
*/
export const adminRoutes: RouteRecordRaw[] = [
{
path: "/admin/login",
name: "admin-login",
component: () => import("@/views/admin/login-view.vue"),
meta: { public: true, world: "admin" },
},
{
path: "/admin",
component: AdminLayout,
meta: { requiresAdmin: true, world: "admin" },
children: [
{
path: "",
redirect: { name: "admin-overview" },
},
{
path: "overview",
name: "admin-overview",
component: () => import("@/views/admin/overview-view.vue"),
},
{
path: "keys",
name: "admin-keys",
component: () => import("@/views/admin/keys-view.vue"),
},
{
path: "payments",
name: "admin-payments",
component: () => import("@/views/admin/payments-view.vue"),
},
{
path: "members",
name: "admin-members",
component: () => import("@/views/admin/members-view.vue"),
},
{
path: "audit",
name: "admin-audit",
component: () => import("@/views/admin/audit-view.vue"),
},
{
path: "settings",
name: "admin-settings",
component: () => import("@/views/admin/settings-view.vue"),
},
],
},
];
+59
View File
@@ -0,0 +1,59 @@
import { createRouter, createWebHistory, type Router } from "vue-router";
import { adminRoutes } from "./admin.ts";
import { memberRoutes } from "./member.ts";
import { setAuthRedirect } from "@/api/auth-redirect.ts";
import { useAdminSessionStore } from "@/stores/admin-session.ts";
import { useMemberTokenStore } from "@/stores/member-token.ts";
declare module "vue-router" {
interface RouteMeta {
public?: boolean;
requiresAdmin?: boolean;
requiresMember?: boolean;
world?: "admin" | "member";
}
}
export const router: Router = createRouter({
history: createWebHistory(),
routes: [
{ path: "/", redirect: "/admin/overview" },
...adminRoutes,
...memberRoutes,
{ path: "/:pathMatch(.*)*", redirect: "/admin/overview" },
],
});
router.beforeEach((to) => {
if (to.meta.public) return true;
const memberToken = useMemberTokenStore();
// 管理面:硬刷新后 Pinia 为空但会话 cookie 仍在——交给 GET /session 与 401 分流裁决
if (to.meta.requiresMember && !memberToken.hasToken) {
return { name: "member-login" };
}
return true;
});
/** 401 分流接入 router(由 main.ts 调用一次) */
export function bindAuthRedirectToRouter(): void {
setAuthRedirect((world, reason) => {
const adminSession = useAdminSessionStore();
const memberToken = useMemberTokenStore();
if (world === "admin") {
adminSession.signOut();
void router.push({
name: "admin-login",
query: reason === "expired" ? { reason: "expired" } : {},
});
} else {
memberToken.clearToken();
void router.push({
name: "member-login",
query: reason === "expired" ? { reason: "expired" } : {},
});
}
});
}
+24
View File
@@ -0,0 +1,24 @@
import type { RouteRecordRaw } from "vue-router";
import MemberLayout from "@/layouts/member-layout.vue";
/** 成员视图 `/member/*`:登录壳 → 单页(交互共性 §1) */
export const memberRoutes: RouteRecordRaw[] = [
{
path: "/member/login",
name: "member-login",
component: () => import("@/views/member/login-view.vue"),
meta: { public: true, world: "member" },
},
{
path: "/member",
component: MemberLayout,
meta: { requiresMember: true, world: "member" },
children: [
{
path: "",
name: "member-home",
component: () => import("@/views/member/home-view.vue"),
},
],
},
];