create-secra 0.1.7 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/antd-adapter-template/README.md +24 -0
  2. package/antd-adapter-template/index.html +16 -0
  3. package/antd-adapter-template/package-lock.json +3420 -0
  4. package/antd-adapter-template/package.json +20 -47
  5. package/antd-adapter-template/src/app/App.tsx +7 -0
  6. package/antd-adapter-template/src/app/layouts/AppLayout.tsx +163 -0
  7. package/antd-adapter-template/src/app/router.ts +26 -0
  8. package/antd-adapter-template/src/app/routes/RouteTitleSync.tsx +26 -0
  9. package/antd-adapter-template/src/app/routes/dynamic-routes.ts +52 -0
  10. package/antd-adapter-template/src/app/routes/route-modules.ts +4 -0
  11. package/antd-adapter-template/src/app/routes/static-routes.tsx +110 -0
  12. package/antd-adapter-template/src/app/routes/types.ts +9 -0
  13. package/antd-adapter-template/src/features/auth/api/auth-api.ts +61 -0
  14. package/antd-adapter-template/src/features/auth/auth-store.ts +125 -0
  15. package/antd-adapter-template/src/features/auth/auth-types.ts +29 -0
  16. package/antd-adapter-template/src/features/auth/authorization.ts +46 -0
  17. package/antd-adapter-template/src/features/auth/use-auth.ts +10 -0
  18. package/antd-adapter-template/src/main.tsx +79 -0
  19. package/antd-adapter-template/src/pages/dashboard/DashboardPage.tsx +105 -0
  20. package/antd-adapter-template/src/pages/errors/ForbiddenPage.tsx +36 -0
  21. package/antd-adapter-template/src/pages/errors/NotFoundPage.tsx +36 -0
  22. package/antd-adapter-template/src/pages/home/HomePage.tsx +129 -0
  23. package/antd-adapter-template/src/pages/login/LoginPage.tsx +128 -0
  24. package/antd-adapter-template/src/pages/permission-test/PermissionTestPage.tsx +55 -0
  25. package/antd-adapter-template/src/pages/restricted/RestrictedDemoPage.tsx +17 -0
  26. package/antd-adapter-template/src/shared/kernel/app-kernel.ts +10 -0
  27. package/antd-adapter-template/src/shared/request/client.ts +46 -0
  28. package/antd-adapter-template/src/shared/request/contracts.ts +6 -0
  29. package/antd-adapter-template/src/shared/request/kv-adapter.ts +14 -0
  30. package/antd-adapter-template/src/shared/request/kv-backend.ts +244 -0
  31. package/antd-adapter-template/src/shared/request/ky-browser-stub.ts +6 -0
  32. package/antd-adapter-template/src/shared/request/undici-browser-stub.ts +4 -0
  33. package/antd-adapter-template/src/styles/global.css +185 -0
  34. package/antd-adapter-template/src/vite-env.d.ts +2 -0
  35. package/antd-adapter-template/tsconfig.app.json +10 -13
  36. package/antd-adapter-template/tsconfig.json +7 -2
  37. package/antd-adapter-template/tsconfig.node.json +6 -16
  38. package/antd-adapter-template/vite.config.ts +24 -0
  39. package/bin/index.mjs +29 -5
  40. package/package.json +2 -2
  41. package/template/apps/core/src/main.tsx +34 -13
  42. package/template/package.json +6 -2
  43. package/template/packages/sdk/package.json +3 -0
  44. package/template/packages/sdk/src/request/index.ts +1 -1
  45. package/template/pnpm-lock.yaml +67 -88
  46. package/antd-adapter-template/apps/core/index.html +0 -13
  47. package/antd-adapter-template/apps/core/package.json +0 -18
  48. package/antd-adapter-template/apps/core/public/favicon.ico +0 -1
  49. package/antd-adapter-template/apps/core/public/favicon.svg +0 -1
  50. package/antd-adapter-template/apps/core/public/logo.svg +0 -1
  51. package/antd-adapter-template/apps/core/src/api/auth.ts +0 -49
  52. package/antd-adapter-template/apps/core/src/assets/react.svg +0 -1
  53. package/antd-adapter-template/apps/core/src/components/AntdGlobalProvider.tsx +0 -87
  54. package/antd-adapter-template/apps/core/src/components/AntdRootLayout.tsx +0 -10
  55. package/antd-adapter-template/apps/core/src/components/layout.tsx +0 -387
  56. package/antd-adapter-template/apps/core/src/guards/auth-route-guard.ts +0 -45
  57. package/antd-adapter-template/apps/core/src/main.tsx +0 -65
  58. package/antd-adapter-template/apps/core/src/pages/auth/components/account-login-fields.tsx +0 -60
  59. package/antd-adapter-template/apps/core/src/pages/auth/components/phone-login-fields.tsx +0 -60
  60. package/antd-adapter-template/apps/core/src/pages/auth/login.tsx +0 -169
  61. package/antd-adapter-template/apps/core/src/pages/index.tsx +0 -156
  62. package/antd-adapter-template/apps/core/src/router.ts +0 -42
  63. package/antd-adapter-template/apps/core/src/shims/use-sync-external-store-shim.ts +0 -3
  64. package/antd-adapter-template/apps/core/src/theme/theme.css +0 -48
  65. package/antd-adapter-template/apps/core/src/types/crypto-js.d.ts +0 -5
  66. package/antd-adapter-template/apps/core/src/utils/index.ts +0 -12
  67. package/antd-adapter-template/apps/core/src/utils/md5.ts +0 -6
  68. package/antd-adapter-template/apps/core/tsconfig.app.json +0 -11
  69. package/antd-adapter-template/apps/core/tsconfig.json +0 -13
  70. package/antd-adapter-template/apps/core/tsconfig.node.json +0 -7
  71. package/antd-adapter-template/apps/core/vite.config.ts +0 -118
  72. package/antd-adapter-template/eslint.config.js +0 -23
  73. package/antd-adapter-template/packages/sdk/.swcrc +0 -18
  74. package/antd-adapter-template/packages/sdk/package.json +0 -52
  75. package/antd-adapter-template/packages/sdk/src/build/index.ts +0 -28
  76. package/antd-adapter-template/packages/sdk/src/build/plugins/auto-import.ts +0 -46
  77. package/antd-adapter-template/packages/sdk/src/build/plugins/bundle-analyzer.ts +0 -33
  78. package/antd-adapter-template/packages/sdk/src/build/plugins/remove-console.ts +0 -23
  79. package/antd-adapter-template/packages/sdk/src/build/plugins/unocss.ts +0 -202
  80. package/antd-adapter-template/packages/sdk/src/build/plugins/unplugin-icon.ts +0 -43
  81. package/antd-adapter-template/packages/sdk/src/components/i18n-switch-dropdown.tsx +0 -139
  82. package/antd-adapter-template/packages/sdk/src/components/index.ts +0 -2
  83. package/antd-adapter-template/packages/sdk/src/components/theme-switch-dropdown.tsx +0 -131
  84. package/antd-adapter-template/packages/sdk/src/hooks/auth/core.ts +0 -101
  85. package/antd-adapter-template/packages/sdk/src/hooks/auth/index.ts +0 -139
  86. package/antd-adapter-template/packages/sdk/src/hooks/auth/with-auth.tsx +0 -41
  87. package/antd-adapter-template/packages/sdk/src/hooks/index.ts +0 -1
  88. package/antd-adapter-template/packages/sdk/src/i18n/index.ts +0 -150
  89. package/antd-adapter-template/packages/sdk/src/index.ts +0 -11
  90. package/antd-adapter-template/packages/sdk/src/request/index.ts +0 -436
  91. package/antd-adapter-template/packages/sdk/src/storage/README.md +0 -30
  92. package/antd-adapter-template/packages/sdk/src/storage/index.ts +0 -57
  93. package/antd-adapter-template/packages/sdk/src/styles/reset.css +0 -111
  94. package/antd-adapter-template/packages/sdk/src/theme/index.ts +0 -466
  95. package/antd-adapter-template/packages/sdk/tsconfig.json +0 -16
  96. package/antd-adapter-template/pnpm-workspace.yaml +0 -3
  97. package/antd-adapter-template/turbo.json +0 -17
@@ -1,63 +1,36 @@
1
1
  {
2
- "name": "secra-admin",
2
+ "name": "secra-antd-demo",
3
3
  "private": true,
4
- "version": "0.0.15",
5
- "packageManager": "pnpm@9.12.3",
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "packageManager": "pnpm@10.27.0",
6
7
  "scripts": {
7
- "build": "turbo run build",
8
- "dev": "turbo run dev",
9
- "lint": "turbo run lint",
10
- "test": "turbo run test",
11
- "clean:node_modules": "find . -type d -name 'node_modules' -prune -exec rm -rf '{}' +",
12
- "build:packages": "pnpm -r --filter \"./packages/*\" build"
8
+ "dev": "vite",
9
+ "build": "tsc -b && vite build",
10
+ "preview": "vite preview",
11
+ "typecheck": "tsc -b --pretty false"
13
12
  },
14
13
  "dependencies": {
15
14
  "@ant-design/icons": "^6.1.0",
16
- "@ant-design/pro-components": "^2.8.10",
17
- "@ant-design/v5-patch-for-react-19": "^1.0.3",
18
- "@vlian/framework": "^1.2.16",
19
- "@vlian/infrastructure": "^0.9.11",
20
- "@vlian/sdk": "workspace:*",
21
- "ahooks": "^3.8.5",
15
+ "@vlian/csrf": "^0.1.2",
16
+ "@vlian/framework": "^2.0.3",
17
+ "@vlian/logger": "^0.1.1",
18
+ "@vlian/monitoring": "^0.1.0",
19
+ "@vlian/request": "^0.1.3",
20
+ "@vlian/router": "^0.1.0",
21
+ "@vlian/utils": "^2.0.1",
22
22
  "antd": "^5.29.3",
23
- "i18next": "^25.7.4",
24
- "immer": "^10.1.3",
25
- "ky": "^1.14.2",
26
- "lodash": "^4.17.21",
27
23
  "react": "^19.2.0",
28
24
  "react-dom": "^19.2.0",
29
- "react-i18next": "^15.7.4",
30
- "react-router-dom": "^7.8.2"
25
+ "react-router-dom": "^7.13.1"
31
26
  },
32
27
  "devDependencies": {
33
- "@eslint/js": "^9.39.1",
34
- "@iconify/utils": "^3.1.0",
35
- "@types/lodash": "^4.17.21",
36
28
  "@types/node": "^24.10.1",
37
- "@types/react": "^19.2.5",
29
+ "@types/react": "^19.2.7",
38
30
  "@types/react-dom": "^19.2.3",
39
- "@unocss/core": "^66.6.0",
40
- "@unocss/preset-attributify": "^66.6.0",
41
- "@unocss/preset-icons": "^66.6.0",
42
- "@unocss/preset-uno": "^66.6.0",
43
- "@unocss/transformer-directives": "^66.6.0",
44
- "@unocss/transformer-variant-group": "^66.6.0",
45
- "@unocss/vite": "^66.6.0",
46
31
  "@vitejs/plugin-react": "^5.1.1",
47
- "eslint": "^9.39.1",
48
- "eslint-plugin-react-hooks": "^7.0.1",
49
- "eslint-plugin-react-refresh": "^0.4.24",
50
- "globals": "^16.5.0",
51
- "gogocode": "^1.0.53",
52
- "rollup-plugin-visualizer": "^5.14.0",
53
- "turbo": "^2.7.5",
54
- "typescript": "~5.9.3",
55
- "typescript-eslint": "^8.46.4",
56
- "unplugin-auto-import": "^0.17.8",
57
- "unplugin-icons": "^0.20.1",
58
- "vite": "npm:rolldown-vite@7.2.5",
59
- "vite-plugin-inspect": "^11.3.3",
60
- "vite-plugin-remove-console": "^1.3.0",
61
- "vite-plugin-svg-icons": "^2.0.1"
32
+ "typescript": "^5.9.3",
33
+ "vite": "npm:rolldown-vite@7.2.5"
62
34
  }
63
35
  }
36
+
@@ -0,0 +1,7 @@
1
+ import { RouterProvider } from "react-router-dom";
2
+ import { router } from "@/app/router";
3
+
4
+ export function App() {
5
+ return <RouterProvider router={router} />;
6
+ }
7
+
@@ -0,0 +1,163 @@
1
+ import {
2
+ DashboardOutlined,
3
+ HomeOutlined,
4
+ LockOutlined,
5
+ LoginOutlined,
6
+ LogoutOutlined,
7
+ SafetyCertificateOutlined,
8
+ UserOutlined,
9
+ } from "@ant-design/icons";
10
+ import { Avatar, Button, Layout, Menu, Space, Tag, Typography, message } from "antd";
11
+ import { type ReactNode, useMemo, useState } from "react";
12
+ import { useLocation, useNavigate } from "react-router-dom";
13
+ import type { RouteLayoutProps } from "@vlian/router/types";
14
+ import { authStore } from "@/features/auth/auth-store";
15
+ import { useAuth } from "@/features/auth/use-auth";
16
+ import type { AppRouteExtra, AppRouteMeta } from "@/app/routes/types";
17
+ import { RouteTitleSync } from "@/app/routes/RouteTitleSync";
18
+
19
+ const { Header, Content } = Layout;
20
+ const { Text, Title } = Typography;
21
+
22
+ type NavigationItem = {
23
+ key: string;
24
+ label: string;
25
+ icon: ReactNode;
26
+ path: string;
27
+ };
28
+
29
+ const navigationItems: NavigationItem[] = [
30
+ {
31
+ key: "home",
32
+ label: "首页",
33
+ icon: <HomeOutlined />,
34
+ path: "/",
35
+ },
36
+ {
37
+ key: "dashboard",
38
+ label: "仪表盘",
39
+ icon: <DashboardOutlined />,
40
+ path: "/dashboard",
41
+ },
42
+ {
43
+ key: "permission-test",
44
+ label: "没有权限测试页",
45
+ icon: <LockOutlined />,
46
+ path: "/permission-test",
47
+ },
48
+ ];
49
+
50
+ const resolveSelectedKey = (pathname: string) => {
51
+ if (pathname.startsWith("/dashboard")) {
52
+ return ["dashboard"];
53
+ }
54
+
55
+ if (pathname.startsWith("/permission-test") || pathname.startsWith("/restricted-demo")) {
56
+ return ["permission-test"];
57
+ }
58
+
59
+ if (pathname === "/") {
60
+ return ["home"];
61
+ }
62
+
63
+ return [];
64
+ };
65
+
66
+ export function AppLayout({
67
+ children,
68
+ }: RouteLayoutProps<AppRouteMeta, AppRouteExtra>) {
69
+ const navigate = useNavigate();
70
+ const location = useLocation();
71
+ const authState = useAuth();
72
+ const [logoutPending, setLogoutPending] = useState(false);
73
+
74
+ const selectedKeys = useMemo(
75
+ () => resolveSelectedKey(location.pathname),
76
+ [location.pathname],
77
+ );
78
+
79
+ const handleLogout = async () => {
80
+ setLogoutPending(true);
81
+
82
+ try {
83
+ await authStore.logout();
84
+ message.success("已退出登录");
85
+ void navigate("/");
86
+ } finally {
87
+ setLogoutPending(false);
88
+ }
89
+ };
90
+
91
+ return (
92
+ <Layout className="app-shell">
93
+ <RouteTitleSync />
94
+ <Header className="app-shell__header">
95
+ <div className="app-shell__brand">
96
+ <div className="app-shell__brand-badge">S</div>
97
+ <div>
98
+ <Title className="app-shell__brand-title" level={4}>
99
+ Secra Antd Demo
100
+ </Title>
101
+ <Text className="app-shell__brand-subtitle">
102
+ 独立模板,动态路由 + 登录态 + 权限模拟
103
+ </Text>
104
+ </div>
105
+ </div>
106
+
107
+ <Menu
108
+ className="app-shell__menu"
109
+ items={navigationItems.map((item) => ({
110
+ key: item.key,
111
+ icon: item.icon,
112
+ label: item.label,
113
+ onClick: () => {
114
+ void navigate(item.path);
115
+ },
116
+ }))}
117
+ mode="horizontal"
118
+ selectedKeys={selectedKeys}
119
+ />
120
+
121
+ <Space size="middle">
122
+ {authState.isAuthenticated ? (
123
+ <>
124
+ <Tag bordered={false} color="blue" icon={<SafetyCertificateOutlined />}>
125
+ 已登录
126
+ </Tag>
127
+ <Space size="small">
128
+ <Avatar icon={<UserOutlined />} />
129
+ <div className="app-shell__user">
130
+ <Text strong>{authState.session?.displayName}</Text>
131
+ <Text type="secondary">{authState.session?.username}</Text>
132
+ </div>
133
+ </Space>
134
+ <Button
135
+ icon={<LogoutOutlined />}
136
+ loading={logoutPending}
137
+ onClick={() => {
138
+ void handleLogout();
139
+ }}
140
+ >
141
+ 退出
142
+ </Button>
143
+ </>
144
+ ) : (
145
+ <Button
146
+ icon={<LoginOutlined />}
147
+ type="primary"
148
+ onClick={() => {
149
+ void navigate("/login");
150
+ }}
151
+ >
152
+ 去登录
153
+ </Button>
154
+ )}
155
+ </Space>
156
+ </Header>
157
+
158
+ <Content className="app-shell__content">
159
+ <div className="page-shell">{children}</div>
160
+ </Content>
161
+ </Layout>
162
+ );
163
+ }
@@ -0,0 +1,26 @@
1
+ import { createAppRouter } from "@vlian/router";
2
+ import { logger } from "@vlian/logger";
3
+ import { authorization } from "@/features/auth/authorization";
4
+ import { loadDynamicRoutes } from "@/app/routes/dynamic-routes";
5
+ import { staticRoutes } from "@/app/routes/static-routes";
6
+ import type { AppRouteExtra, AppRouteMeta } from "@/app/routes/types";
7
+
8
+ export const router = createAppRouter<AppRouteMeta, AppRouteExtra>({
9
+ mode: "browser",
10
+ routes: staticRoutes,
11
+ authorization,
12
+ dynamic: {
13
+ defaultParentId: "root",
14
+ loader: loadDynamicRoutes,
15
+ getNavigationCacheKey: ({ path }) =>
16
+ path.startsWith("/dashboard") ? "dynamic:dashboard" : false,
17
+ errorMode: "throw",
18
+ },
19
+ onError: ({ error, path }) => {
20
+ logger.error("router.dynamic.failed", {
21
+ path,
22
+ error,
23
+ });
24
+ },
25
+ });
26
+
@@ -0,0 +1,26 @@
1
+ import { useEffect } from "react";
2
+ import { useMatches } from "react-router-dom";
3
+
4
+ type RouteHandle = {
5
+ meta?: {
6
+ title?: string;
7
+ };
8
+ };
9
+
10
+ export function RouteTitleSync() {
11
+ const matches = useMatches();
12
+
13
+ useEffect(() => {
14
+ const routeTitle = [...matches]
15
+ .reverse()
16
+ .map((match) => (match.handle as RouteHandle | undefined)?.meta?.title)
17
+ .find(Boolean);
18
+
19
+ document.title = routeTitle
20
+ ? `${routeTitle} | Secra Antd Demo`
21
+ : "Secra Antd Demo";
22
+ }, [matches]);
23
+
24
+ return null;
25
+ }
26
+
@@ -0,0 +1,52 @@
1
+ import type { DynamicRouteLoader } from "@vlian/router";
2
+ import { logger } from "@vlian/logger";
3
+ import { authApi } from "@/features/auth/api/auth-api";
4
+ import type { DynamicRouteManifestItem } from "@/features/auth/auth-types";
5
+ import { pageModuleLoaders } from "@/app/routes/route-modules";
6
+ import type { AppRouteExtra, AppRouteMeta } from "@/app/routes/types";
7
+
8
+ export const loadDynamicRoutes: DynamicRouteLoader<
9
+ AppRouteMeta,
10
+ AppRouteExtra
11
+ > = async ({ path }) => {
12
+ if (!path.startsWith("/dashboard")) {
13
+ return;
14
+ }
15
+
16
+ const manifest = await authApi.getDynamicRoutes();
17
+
18
+ logger.info("router.dynamic.loaded", {
19
+ path,
20
+ count: manifest.length,
21
+ });
22
+
23
+ return {
24
+ parentId: "root",
25
+ cacheKey: "dynamic:dashboard:manifest",
26
+ routes: manifest.map((item: DynamicRouteManifestItem) => ({
27
+ id: item.code,
28
+ path: item.path.replace(/^\//, ""),
29
+ menuKey: "dashboard",
30
+ auth: {
31
+ required: item.requiresAuth,
32
+ redirectTo: "/login",
33
+ },
34
+ meta: {
35
+ title: item.title,
36
+ description: "通过 @vlian/router dynamic.loader 动态注入",
37
+ },
38
+ lazy: async () => {
39
+ const loader = pageModuleLoaders[item.component];
40
+
41
+ if (!loader) {
42
+ throw new Error(`Unknown dynamic page module: ${item.component}`);
43
+ }
44
+
45
+ const mod = await loader();
46
+ return {
47
+ Component: mod.default,
48
+ };
49
+ },
50
+ })),
51
+ };
52
+ };
@@ -0,0 +1,4 @@
1
+ export const pageModuleLoaders: Record<string, () => Promise<any>> = {
2
+ dashboard: () => import("@/pages/dashboard/DashboardPage"),
3
+ };
4
+
@@ -0,0 +1,110 @@
1
+ import { Outlet } from "react-router-dom";
2
+ import { defineRoutes } from "@vlian/router";
3
+ import { AppLayout } from "@/app/layouts/AppLayout";
4
+ import type { AppRouteExtra, AppRouteMeta } from "@/app/routes/types";
5
+
6
+ const loadHomePage = () => import("@/pages/home/HomePage");
7
+ const loadLoginPage = () => import("@/pages/login/LoginPage");
8
+ const loadForbiddenPage = () => import("@/pages/errors/ForbiddenPage");
9
+ const loadNotFoundPage = () => import("@/pages/errors/NotFoundPage");
10
+ const loadPermissionTestPage = () => import("@/pages/permission-test/PermissionTestPage");
11
+ const loadRestrictedPage = () => import("@/pages/restricted/RestrictedDemoPage");
12
+
13
+ export const staticRoutes = defineRoutes<AppRouteMeta, AppRouteExtra>([
14
+ {
15
+ id: "root",
16
+ path: "/",
17
+ component: Outlet,
18
+ layout: AppLayout,
19
+ children: [
20
+ {
21
+ index: true,
22
+ menuKey: "home",
23
+ meta: {
24
+ title: "首页",
25
+ description: "模板首页",
26
+ },
27
+ lazy: async () => {
28
+ const mod = await loadHomePage();
29
+ return {
30
+ Component: mod.default,
31
+ };
32
+ },
33
+ },
34
+ {
35
+ path: "permission-test",
36
+ menuKey: "permission-test",
37
+ auth: {
38
+ required: true,
39
+ redirectTo: "/login",
40
+ },
41
+ meta: {
42
+ title: "没有权限测试页",
43
+ description: "用于跳转到一个受限资源,模拟 403 场景",
44
+ },
45
+ lazy: async () => {
46
+ const mod = await loadPermissionTestPage();
47
+ return {
48
+ Component: mod.default,
49
+ };
50
+ },
51
+ },
52
+ {
53
+ path: "restricted-demo",
54
+ auth: {
55
+ required: true,
56
+ permissionCode: "system:restricted",
57
+ redirectTo: "/403",
58
+ },
59
+ meta: {
60
+ title: "受限资源",
61
+ description: "正常情况下会被权限拦截到 403",
62
+ },
63
+ lazy: async () => {
64
+ const mod = await loadRestrictedPage();
65
+ return {
66
+ Component: mod.default,
67
+ };
68
+ },
69
+ },
70
+ {
71
+ path: "403",
72
+ meta: {
73
+ title: "403 禁止访问",
74
+ },
75
+ lazy: async () => {
76
+ const mod = await loadForbiddenPage();
77
+ return {
78
+ Component: mod.default,
79
+ };
80
+ },
81
+ },
82
+ ],
83
+ },
84
+ {
85
+ id: "login",
86
+ path: "/login",
87
+ meta: {
88
+ title: "登录",
89
+ },
90
+ lazy: async () => {
91
+ const mod = await loadLoginPage();
92
+ return {
93
+ Component: mod.default,
94
+ };
95
+ },
96
+ },
97
+ {
98
+ id: "not-found",
99
+ path: "/404",
100
+ meta: {
101
+ title: "404 页面不存在",
102
+ },
103
+ lazy: async () => {
104
+ const mod = await loadNotFoundPage();
105
+ return {
106
+ Component: mod.default,
107
+ };
108
+ },
109
+ },
110
+ ]);
@@ -0,0 +1,9 @@
1
+ export interface AppRouteMeta {
2
+ title: string;
3
+ description?: string;
4
+ }
5
+
6
+ export interface AppRouteExtra {
7
+ [key: string]: unknown;
8
+ menuKey?: "home" | "dashboard" | "permission-test";
9
+ }
@@ -0,0 +1,61 @@
1
+ import { logger } from "@vlian/logger";
2
+ import { getAppCache } from "@/shared/kernel/app-kernel";
3
+ import { kvRequestClient } from "@/shared/request/client";
4
+ import type { ApiEnvelope } from "@/shared/request/contracts";
5
+ import type {
6
+ AuthSession,
7
+ DynamicRouteManifestItem,
8
+ LoginPayload,
9
+ } from "@/features/auth/auth-types";
10
+
11
+ const unwrapEnvelope = <T>(payload: ApiEnvelope<T>, logMessage: string) => {
12
+ if (payload.code !== 0) {
13
+ logger.warn(logMessage, payload);
14
+ throw new Error(payload.message || "请求失败");
15
+ }
16
+
17
+ return payload.data;
18
+ };
19
+
20
+ export const authApi = {
21
+ async login(payload: LoginPayload): Promise<AuthSession> {
22
+ const response = await kvRequestClient.post<AuthSession, LoginPayload>(
23
+ "/api/auth/login",
24
+ payload,
25
+ );
26
+
27
+ const session = unwrapEnvelope(response, "auth.login.failed");
28
+ const cache = getAppCache();
29
+
30
+ if (cache) {
31
+ await cache.set("last-login-user", session.displayName, {
32
+ expire: -1,
33
+ });
34
+ }
35
+
36
+ logger.info("auth.login.success", {
37
+ username: session.username,
38
+ });
39
+
40
+ return session;
41
+ },
42
+
43
+ async getSession(): Promise<AuthSession | null> {
44
+ const response = await kvRequestClient.get<AuthSession | null>("/api/auth/session");
45
+ return unwrapEnvelope(response, "auth.session.failed");
46
+ },
47
+
48
+ async logout(): Promise<void> {
49
+ const response = await kvRequestClient.post<null, null>(
50
+ "/api/auth/logout",
51
+ null,
52
+ );
53
+ unwrapEnvelope(response, "auth.logout.failed");
54
+ logger.info("auth.logout.success");
55
+ },
56
+
57
+ async getDynamicRoutes(): Promise<DynamicRouteManifestItem[]> {
58
+ const response = await kvRequestClient.get<DynamicRouteManifestItem[]>("/api/routes");
59
+ return unwrapEnvelope(response, "auth.routes.failed");
60
+ },
61
+ };
@@ -0,0 +1,125 @@
1
+ import { logger } from "@vlian/logger";
2
+ import { authApi } from "@/features/auth/api/auth-api";
3
+ import type { AuthSession, AuthState, LoginPayload } from "@/features/auth/auth-types";
4
+
5
+ type Listener = () => void;
6
+
7
+ let state: AuthState = {
8
+ ready: false,
9
+ pending: false,
10
+ session: null,
11
+ permissions: [],
12
+ isAuthenticated: false,
13
+ };
14
+
15
+ let bootstrapPromise: Promise<void> | null = null;
16
+ const listeners = new Set<Listener>();
17
+
18
+ const emitChange = () => {
19
+ listeners.forEach((listener) => listener());
20
+ };
21
+
22
+ const applySession = (session: AuthSession | null) => {
23
+ state = {
24
+ ...state,
25
+ session,
26
+ permissions: session?.permissions ?? [],
27
+ isAuthenticated: Boolean(session?.token),
28
+ };
29
+ };
30
+
31
+ const setState = (partial: Partial<AuthState>) => {
32
+ state = {
33
+ ...state,
34
+ ...partial,
35
+ };
36
+ emitChange();
37
+ };
38
+
39
+ export const authStore = {
40
+ subscribe(listener: Listener) {
41
+ listeners.add(listener);
42
+ return () => {
43
+ listeners.delete(listener);
44
+ };
45
+ },
46
+
47
+ getSnapshot(): AuthState {
48
+ return state;
49
+ },
50
+
51
+ async bootstrap() {
52
+ if (state.ready) {
53
+ return;
54
+ }
55
+
56
+ if (!bootstrapPromise) {
57
+ bootstrapPromise = (async () => {
58
+ setState({
59
+ pending: true,
60
+ });
61
+
62
+ try {
63
+ const session = await authApi.getSession();
64
+ applySession(session);
65
+ setState({
66
+ ready: true,
67
+ pending: false,
68
+ });
69
+ logger.debug("auth.bootstrap.done", {
70
+ authenticated: Boolean(session),
71
+ });
72
+ } catch (error) {
73
+ applySession(null);
74
+ setState({
75
+ ready: true,
76
+ pending: false,
77
+ });
78
+ logger.error("auth.bootstrap.failed", error);
79
+ }
80
+ })().finally(() => {
81
+ bootstrapPromise = null;
82
+ });
83
+ }
84
+
85
+ await bootstrapPromise;
86
+ },
87
+
88
+ async login(payload: LoginPayload) {
89
+ setState({
90
+ pending: true,
91
+ });
92
+
93
+ try {
94
+ const session = await authApi.login(payload);
95
+ applySession(session);
96
+ setState({
97
+ ready: true,
98
+ pending: false,
99
+ });
100
+ return session;
101
+ } catch (error) {
102
+ setState({
103
+ pending: false,
104
+ });
105
+ throw error;
106
+ }
107
+ },
108
+
109
+ async logout() {
110
+ setState({
111
+ pending: true,
112
+ });
113
+
114
+ try {
115
+ await authApi.logout();
116
+ } finally {
117
+ applySession(null);
118
+ setState({
119
+ ready: true,
120
+ pending: false,
121
+ });
122
+ }
123
+ },
124
+ };
125
+