create-secra 0.1.11 → 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.
- package/antd-adapter-template/README.md +24 -0
- package/antd-adapter-template/index.html +16 -0
- package/antd-adapter-template/package-lock.json +3420 -0
- package/antd-adapter-template/package.json +20 -47
- package/antd-adapter-template/src/app/App.tsx +7 -0
- package/antd-adapter-template/src/app/layouts/AppLayout.tsx +163 -0
- package/antd-adapter-template/src/app/router.ts +26 -0
- package/antd-adapter-template/src/app/routes/RouteTitleSync.tsx +26 -0
- package/antd-adapter-template/src/app/routes/dynamic-routes.ts +52 -0
- package/antd-adapter-template/src/app/routes/route-modules.ts +4 -0
- package/antd-adapter-template/src/app/routes/static-routes.tsx +110 -0
- package/antd-adapter-template/src/app/routes/types.ts +9 -0
- package/antd-adapter-template/src/features/auth/api/auth-api.ts +61 -0
- package/antd-adapter-template/src/features/auth/auth-store.ts +125 -0
- package/antd-adapter-template/src/features/auth/auth-types.ts +29 -0
- package/antd-adapter-template/src/features/auth/authorization.ts +46 -0
- package/antd-adapter-template/src/features/auth/use-auth.ts +10 -0
- package/antd-adapter-template/src/main.tsx +79 -0
- package/antd-adapter-template/src/pages/dashboard/DashboardPage.tsx +105 -0
- package/antd-adapter-template/src/pages/errors/ForbiddenPage.tsx +36 -0
- package/antd-adapter-template/src/pages/errors/NotFoundPage.tsx +36 -0
- package/antd-adapter-template/src/pages/home/HomePage.tsx +129 -0
- package/antd-adapter-template/src/pages/login/LoginPage.tsx +128 -0
- package/antd-adapter-template/src/pages/permission-test/PermissionTestPage.tsx +55 -0
- package/antd-adapter-template/src/pages/restricted/RestrictedDemoPage.tsx +17 -0
- package/antd-adapter-template/src/shared/kernel/app-kernel.ts +10 -0
- package/antd-adapter-template/src/shared/request/client.ts +46 -0
- package/antd-adapter-template/src/shared/request/contracts.ts +6 -0
- package/antd-adapter-template/src/shared/request/kv-adapter.ts +14 -0
- package/antd-adapter-template/src/shared/request/kv-backend.ts +244 -0
- package/antd-adapter-template/src/shared/request/ky-browser-stub.ts +6 -0
- package/antd-adapter-template/src/shared/request/undici-browser-stub.ts +4 -0
- package/antd-adapter-template/src/styles/global.css +185 -0
- package/antd-adapter-template/src/vite-env.d.ts +2 -0
- package/antd-adapter-template/tsconfig.app.json +10 -13
- package/antd-adapter-template/tsconfig.json +7 -2
- package/antd-adapter-template/tsconfig.node.json +6 -16
- package/antd-adapter-template/vite.config.ts +24 -0
- package/bin/index.mjs +29 -5
- package/package.json +2 -2
- package/template/apps/core/src/main.tsx +34 -13
- package/template/package.json +6 -2
- package/template/packages/sdk/package.json +3 -0
- package/template/packages/sdk/src/request/index.ts +1 -1
- package/template/pnpm-lock.yaml +67 -88
- package/antd-adapter-template/apps/core/index.html +0 -13
- package/antd-adapter-template/apps/core/package.json +0 -18
- package/antd-adapter-template/apps/core/public/favicon.ico +0 -1
- package/antd-adapter-template/apps/core/public/favicon.svg +0 -1
- package/antd-adapter-template/apps/core/public/logo.svg +0 -1
- package/antd-adapter-template/apps/core/src/api/auth.ts +0 -49
- package/antd-adapter-template/apps/core/src/assets/react.svg +0 -1
- package/antd-adapter-template/apps/core/src/components/AntdGlobalProvider.tsx +0 -87
- package/antd-adapter-template/apps/core/src/components/AntdRootLayout.tsx +0 -10
- package/antd-adapter-template/apps/core/src/components/layout.tsx +0 -387
- package/antd-adapter-template/apps/core/src/guards/auth-route-guard.ts +0 -45
- package/antd-adapter-template/apps/core/src/main.tsx +0 -65
- package/antd-adapter-template/apps/core/src/pages/auth/components/account-login-fields.tsx +0 -60
- package/antd-adapter-template/apps/core/src/pages/auth/components/phone-login-fields.tsx +0 -60
- package/antd-adapter-template/apps/core/src/pages/auth/login.tsx +0 -169
- package/antd-adapter-template/apps/core/src/pages/index.tsx +0 -156
- package/antd-adapter-template/apps/core/src/router.ts +0 -42
- package/antd-adapter-template/apps/core/src/shims/use-sync-external-store-shim.ts +0 -3
- package/antd-adapter-template/apps/core/src/theme/theme.css +0 -48
- package/antd-adapter-template/apps/core/src/types/crypto-js.d.ts +0 -5
- package/antd-adapter-template/apps/core/src/utils/index.ts +0 -12
- package/antd-adapter-template/apps/core/src/utils/md5.ts +0 -6
- package/antd-adapter-template/apps/core/tsconfig.app.json +0 -11
- package/antd-adapter-template/apps/core/tsconfig.json +0 -13
- package/antd-adapter-template/apps/core/tsconfig.node.json +0 -7
- package/antd-adapter-template/apps/core/vite.config.ts +0 -118
- package/antd-adapter-template/eslint.config.js +0 -23
- package/antd-adapter-template/packages/sdk/.swcrc +0 -18
- package/antd-adapter-template/packages/sdk/package.json +0 -52
- package/antd-adapter-template/packages/sdk/src/build/index.ts +0 -28
- package/antd-adapter-template/packages/sdk/src/build/plugins/auto-import.ts +0 -46
- package/antd-adapter-template/packages/sdk/src/build/plugins/bundle-analyzer.ts +0 -33
- package/antd-adapter-template/packages/sdk/src/build/plugins/remove-console.ts +0 -23
- package/antd-adapter-template/packages/sdk/src/build/plugins/unocss.ts +0 -202
- package/antd-adapter-template/packages/sdk/src/build/plugins/unplugin-icon.ts +0 -43
- package/antd-adapter-template/packages/sdk/src/components/i18n-switch-dropdown.tsx +0 -139
- package/antd-adapter-template/packages/sdk/src/components/index.ts +0 -2
- package/antd-adapter-template/packages/sdk/src/components/theme-switch-dropdown.tsx +0 -131
- package/antd-adapter-template/packages/sdk/src/hooks/auth/core.ts +0 -101
- package/antd-adapter-template/packages/sdk/src/hooks/auth/index.ts +0 -139
- package/antd-adapter-template/packages/sdk/src/hooks/auth/with-auth.tsx +0 -41
- package/antd-adapter-template/packages/sdk/src/hooks/index.ts +0 -1
- package/antd-adapter-template/packages/sdk/src/i18n/index.ts +0 -150
- package/antd-adapter-template/packages/sdk/src/index.ts +0 -11
- package/antd-adapter-template/packages/sdk/src/request/index.ts +0 -436
- package/antd-adapter-template/packages/sdk/src/storage/README.md +0 -30
- package/antd-adapter-template/packages/sdk/src/storage/index.ts +0 -57
- package/antd-adapter-template/packages/sdk/src/styles/reset.css +0 -111
- package/antd-adapter-template/packages/sdk/src/theme/index.ts +0 -466
- package/antd-adapter-template/packages/sdk/tsconfig.json +0 -16
- package/antd-adapter-template/pnpm-workspace.yaml +0 -3
- package/antd-adapter-template/turbo.json +0 -17
|
@@ -1,63 +1,36 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "secra-
|
|
2
|
+
"name": "secra-antd-demo",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "0.0.
|
|
5
|
-
"
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@10.27.0",
|
|
6
7
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
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
|
-
"@
|
|
17
|
-
"@
|
|
18
|
-
"@vlian/
|
|
19
|
-
"@vlian/
|
|
20
|
-
"@vlian/
|
|
21
|
-
"
|
|
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-
|
|
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.
|
|
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
|
-
"
|
|
48
|
-
"
|
|
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,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,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,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
|
+
|