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.
- 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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface LoginPayload {
|
|
2
|
+
username: string;
|
|
3
|
+
password: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface AuthSession {
|
|
7
|
+
token: string;
|
|
8
|
+
username: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
permissions: string[];
|
|
11
|
+
loginAt: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DynamicRouteManifestItem {
|
|
15
|
+
code: string;
|
|
16
|
+
path: string;
|
|
17
|
+
title: string;
|
|
18
|
+
component: string;
|
|
19
|
+
requiresAuth: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AuthState {
|
|
23
|
+
ready: boolean;
|
|
24
|
+
pending: boolean;
|
|
25
|
+
session: AuthSession | null;
|
|
26
|
+
permissions: string[];
|
|
27
|
+
isAuthenticated: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { RouteAuthorizationOptions } from "@vlian/router";
|
|
2
|
+
import { authStore } from "@/features/auth/auth-store";
|
|
3
|
+
import type { AppRouteExtra, AppRouteMeta } from "@/app/routes/types";
|
|
4
|
+
|
|
5
|
+
const resolvePermissionCode = (auth: unknown): string | null => {
|
|
6
|
+
if (!auth || typeof auth !== "object") {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const permissionCode = (auth as { permissionCode?: unknown }).permissionCode;
|
|
11
|
+
return typeof permissionCode === "string" ? permissionCode : null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const isRequiredAuth = (auth: unknown) => {
|
|
15
|
+
if (typeof auth === "boolean") {
|
|
16
|
+
return auth;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (auth && typeof auth === "object") {
|
|
20
|
+
const required = (auth as { required?: unknown }).required;
|
|
21
|
+
return required !== false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return false;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const authorization: RouteAuthorizationOptions<
|
|
28
|
+
AppRouteMeta,
|
|
29
|
+
AppRouteExtra
|
|
30
|
+
> = {
|
|
31
|
+
resolve: ({ auth }) => {
|
|
32
|
+
const snapshot = authStore.getSnapshot();
|
|
33
|
+
|
|
34
|
+
if (isRequiredAuth(auth) && !snapshot.isAuthenticated) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const permissionCode = resolvePermissionCode(auth);
|
|
39
|
+
if (!permissionCode) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return snapshot.permissions.includes(permissionCode);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { StrictMode } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { App as AntdApp, ConfigProvider } from "antd";
|
|
5
|
+
import { kernelStartApp, type KernelRenderContext } from "@vlian/framework/kernel";
|
|
6
|
+
import { LogLevel } from "@vlian/logger";
|
|
7
|
+
import { App } from "@/app/App";
|
|
8
|
+
import { authStore } from "@/features/auth/auth-store";
|
|
9
|
+
import { APP_INSTANCE_ID } from "@/shared/kernel/app-kernel";
|
|
10
|
+
import "@/styles/global.css";
|
|
11
|
+
import "antd/dist/reset.css";
|
|
12
|
+
|
|
13
|
+
const container = document.getElementById("root");
|
|
14
|
+
|
|
15
|
+
if (!("React" in globalThis)) {
|
|
16
|
+
// Compatibility shim for @vlian/router 0.1.0, whose published bundle
|
|
17
|
+
// still emits React.createElement in route shell helpers.
|
|
18
|
+
(
|
|
19
|
+
globalThis as typeof globalThis & {
|
|
20
|
+
React?: typeof React;
|
|
21
|
+
}
|
|
22
|
+
).React = React;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!container) {
|
|
26
|
+
throw new Error("Root container #root was not found.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const root = createRoot(container);
|
|
30
|
+
|
|
31
|
+
const startApp = async () => {
|
|
32
|
+
await kernelStartApp(
|
|
33
|
+
{
|
|
34
|
+
config: {
|
|
35
|
+
logger: {
|
|
36
|
+
level: import.meta.env.PROD ? LogLevel.INFO : LogLevel.DEBUG,
|
|
37
|
+
},
|
|
38
|
+
cache: {
|
|
39
|
+
storageOptions: {
|
|
40
|
+
type: "local",
|
|
41
|
+
prefix: "secra-antd-template",
|
|
42
|
+
defaultExpire: 7 * 24 * 60 * 60 * 1000,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
hooks: {
|
|
47
|
+
initialize: async () => {
|
|
48
|
+
await authStore.bootstrap();
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
render: (_context: KernelRenderContext) => {
|
|
52
|
+
root.render(
|
|
53
|
+
<StrictMode>
|
|
54
|
+
<ConfigProvider
|
|
55
|
+
theme={{
|
|
56
|
+
token: {
|
|
57
|
+
colorPrimary: "#155eef",
|
|
58
|
+
colorSuccess: "#157f3b",
|
|
59
|
+
colorWarning: "#c87518",
|
|
60
|
+
colorError: "#c7364f",
|
|
61
|
+
borderRadius: 18,
|
|
62
|
+
fontFamily:
|
|
63
|
+
'"Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif',
|
|
64
|
+
},
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<AntdApp>
|
|
68
|
+
<App />
|
|
69
|
+
</AntdApp>
|
|
70
|
+
</ConfigProvider>
|
|
71
|
+
</StrictMode>,
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
APP_INSTANCE_ID,
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
void startApp();
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BarChartOutlined,
|
|
3
|
+
CheckCircleOutlined,
|
|
4
|
+
DatabaseOutlined,
|
|
5
|
+
RocketOutlined,
|
|
6
|
+
} from "@ant-design/icons";
|
|
7
|
+
import { Alert, Card, Col, Descriptions, Row, Space, Statistic, Tag, Typography } from "antd";
|
|
8
|
+
import { useAuth } from "@/features/auth/use-auth";
|
|
9
|
+
|
|
10
|
+
const { Paragraph, Title, Text } = Typography;
|
|
11
|
+
|
|
12
|
+
const dashboardMetrics = [
|
|
13
|
+
{
|
|
14
|
+
title: "今日请求数",
|
|
15
|
+
value: 128,
|
|
16
|
+
prefix: <BarChartOutlined />,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
title: "路由注入耗时",
|
|
20
|
+
value: 188,
|
|
21
|
+
suffix: "ms",
|
|
22
|
+
prefix: <RocketOutlined />,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
title: "可用 mock 资源",
|
|
26
|
+
value: 4,
|
|
27
|
+
prefix: <DatabaseOutlined />,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
title: "服务状态",
|
|
31
|
+
value: 100,
|
|
32
|
+
suffix: "%",
|
|
33
|
+
prefix: <CheckCircleOutlined />,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export default function DashboardPage() {
|
|
38
|
+
const authState = useAuth();
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Space className="page-stack" direction="vertical" size={24}>
|
|
42
|
+
<Card className="page-hero" variant="borderless">
|
|
43
|
+
<Tag color="processing">动态路由页面</Tag>
|
|
44
|
+
<Title level={2}>仪表盘</Title>
|
|
45
|
+
<Paragraph>
|
|
46
|
+
这个页面不是静态注册的,而是在访问 <Text code>/dashboard</Text> 时通过
|
|
47
|
+
<Text code> @vlian/router </Text>
|
|
48
|
+
的动态加载器注入进来的。登录成功后才允许跳转进来。
|
|
49
|
+
</Paragraph>
|
|
50
|
+
</Card>
|
|
51
|
+
|
|
52
|
+
<Row gutter={[16, 16]}>
|
|
53
|
+
{dashboardMetrics.map((item) => (
|
|
54
|
+
<Col key={item.title} md={12} xl={6} xs={24}>
|
|
55
|
+
<Card className="surface-card" variant="borderless">
|
|
56
|
+
<Statistic
|
|
57
|
+
prefix={item.prefix}
|
|
58
|
+
suffix={item.suffix}
|
|
59
|
+
title={item.title}
|
|
60
|
+
value={item.value}
|
|
61
|
+
/>
|
|
62
|
+
</Card>
|
|
63
|
+
</Col>
|
|
64
|
+
))}
|
|
65
|
+
</Row>
|
|
66
|
+
|
|
67
|
+
<Row gutter={[16, 16]}>
|
|
68
|
+
<Col lg={14} xs={24}>
|
|
69
|
+
<Card className="surface-card" title="当前会话" variant="borderless">
|
|
70
|
+
<Descriptions column={1} size="small">
|
|
71
|
+
<Descriptions.Item label="显示名称">
|
|
72
|
+
{authState.session?.displayName}
|
|
73
|
+
</Descriptions.Item>
|
|
74
|
+
<Descriptions.Item label="账号">
|
|
75
|
+
{authState.session?.username}
|
|
76
|
+
</Descriptions.Item>
|
|
77
|
+
<Descriptions.Item label="登录时间">
|
|
78
|
+
{authState.session?.loginAt}
|
|
79
|
+
</Descriptions.Item>
|
|
80
|
+
<Descriptions.Item label="权限">
|
|
81
|
+
<Space size={[8, 8]} wrap>
|
|
82
|
+
{authState.permissions.map((permission) => (
|
|
83
|
+
<Tag key={permission}>{permission}</Tag>
|
|
84
|
+
))}
|
|
85
|
+
</Space>
|
|
86
|
+
</Descriptions.Item>
|
|
87
|
+
</Descriptions>
|
|
88
|
+
</Card>
|
|
89
|
+
</Col>
|
|
90
|
+
|
|
91
|
+
<Col lg={10} xs={24}>
|
|
92
|
+
<Card className="surface-card" title="路由说明" variant="borderless">
|
|
93
|
+
<Alert
|
|
94
|
+
message="当前仪表盘使用动态路由"
|
|
95
|
+
description="`/dashboard` 对应的页面模块来自 mock 路由清单接口,接口由 @vlian/request 的 kv 适配器模拟。"
|
|
96
|
+
showIcon
|
|
97
|
+
type="info"
|
|
98
|
+
/>
|
|
99
|
+
</Card>
|
|
100
|
+
</Col>
|
|
101
|
+
</Row>
|
|
102
|
+
</Space>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Button, Result, Space } from "antd";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
export default function ForbiddenPage() {
|
|
5
|
+
const navigate = useNavigate();
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="center-page">
|
|
9
|
+
<Result
|
|
10
|
+
extra={
|
|
11
|
+
<Space>
|
|
12
|
+
<Button
|
|
13
|
+
onClick={() => {
|
|
14
|
+
void navigate("/");
|
|
15
|
+
}}
|
|
16
|
+
>
|
|
17
|
+
返回首页
|
|
18
|
+
</Button>
|
|
19
|
+
<Button
|
|
20
|
+
type="primary"
|
|
21
|
+
onClick={() => {
|
|
22
|
+
void navigate("/permission-test");
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
回到测试页
|
|
26
|
+
</Button>
|
|
27
|
+
</Space>
|
|
28
|
+
}
|
|
29
|
+
status="403"
|
|
30
|
+
subTitle="当前账号缺少访问该资源的权限,本次跳转由路由授权逻辑拦截。"
|
|
31
|
+
title="403"
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Button, Result, Space } from "antd";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
export default function NotFoundPage() {
|
|
5
|
+
const navigate = useNavigate();
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="center-page">
|
|
9
|
+
<Result
|
|
10
|
+
extra={
|
|
11
|
+
<Space>
|
|
12
|
+
<Button
|
|
13
|
+
type="primary"
|
|
14
|
+
onClick={() => {
|
|
15
|
+
void navigate("/");
|
|
16
|
+
}}
|
|
17
|
+
>
|
|
18
|
+
返回首页
|
|
19
|
+
</Button>
|
|
20
|
+
<Button
|
|
21
|
+
onClick={() => {
|
|
22
|
+
void navigate("/dashboard");
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
前往仪表盘
|
|
26
|
+
</Button>
|
|
27
|
+
</Space>
|
|
28
|
+
}
|
|
29
|
+
status="404"
|
|
30
|
+
subTitle="当前地址没有命中任何页面,你可以回到首页或尝试访问动态仪表盘路由。"
|
|
31
|
+
title="404"
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApartmentOutlined,
|
|
3
|
+
LockOutlined,
|
|
4
|
+
LoginOutlined,
|
|
5
|
+
ThunderboltOutlined,
|
|
6
|
+
} from "@ant-design/icons";
|
|
7
|
+
import { Button, Card, Col, Row, Space, Tag, Typography } from "antd";
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
import { useNavigate } from "react-router-dom";
|
|
10
|
+
import { useAuth } from "@/features/auth/use-auth";
|
|
11
|
+
import { getAppCache } from "@/shared/kernel/app-kernel";
|
|
12
|
+
|
|
13
|
+
const { Paragraph, Text, Title } = Typography;
|
|
14
|
+
|
|
15
|
+
const highlights = [
|
|
16
|
+
{
|
|
17
|
+
title: "登录页",
|
|
18
|
+
description: "使用 @vlian/request 的 kv 适配器模拟账号登录。",
|
|
19
|
+
icon: <LoginOutlined />,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
title: "动态仪表盘",
|
|
23
|
+
description: "访问 /dashboard 时才注入路由,未登录会跳去登录页。",
|
|
24
|
+
icon: <ThunderboltOutlined />,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
title: "权限测试",
|
|
28
|
+
description: "从测试页进入受限资源,触发 403 页面。",
|
|
29
|
+
icon: <LockOutlined />,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
title: "独立模板",
|
|
33
|
+
description: "模板根目录就是一个可直接安装运行的 Vite 应用。",
|
|
34
|
+
icon: <ApartmentOutlined />,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export default function HomePage() {
|
|
39
|
+
const navigate = useNavigate();
|
|
40
|
+
const authState = useAuth();
|
|
41
|
+
const [lastLoginUser, setLastLoginUser] = useState<string>("暂无记录");
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
let mounted = true;
|
|
45
|
+
|
|
46
|
+
const readCache = async () => {
|
|
47
|
+
const cache = getAppCache();
|
|
48
|
+
|
|
49
|
+
if (!cache) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const cachedUser = await cache.get<string>("last-login-user", {
|
|
54
|
+
defaultValue: "暂无记录",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (mounted) {
|
|
58
|
+
setLastLoginUser(cachedUser || "暂无记录");
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
void readCache();
|
|
63
|
+
|
|
64
|
+
return () => {
|
|
65
|
+
mounted = false;
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Space className="page-stack" direction="vertical" size={24}>
|
|
71
|
+
<Card className="page-hero" variant="borderless">
|
|
72
|
+
<Space direction="vertical" size={16}>
|
|
73
|
+
<Tag color="geekblue">React + TypeScript + Ant Design</Tag>
|
|
74
|
+
<Title level={2}>Secra Antd Adapter Template</Title>
|
|
75
|
+
<Paragraph>
|
|
76
|
+
这里演示了一个不依赖 monorepo 的独立项目结构,同时串起
|
|
77
|
+
<Text code>@vlian/router</Text>、
|
|
78
|
+
<Text code>@vlian/framework</Text>、
|
|
79
|
+
<Text code>@vlian/logger</Text>、
|
|
80
|
+
<Text code>@vlian/request</Text>、
|
|
81
|
+
<Text code>@vlian/utils</Text> 的基础用法。
|
|
82
|
+
</Paragraph>
|
|
83
|
+
|
|
84
|
+
<Space size="middle" wrap>
|
|
85
|
+
<Button
|
|
86
|
+
size="large"
|
|
87
|
+
type="primary"
|
|
88
|
+
onClick={() => {
|
|
89
|
+
void navigate("/dashboard");
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
进入仪表盘
|
|
93
|
+
</Button>
|
|
94
|
+
<Button
|
|
95
|
+
size="large"
|
|
96
|
+
onClick={() => {
|
|
97
|
+
void navigate(authState.isAuthenticated ? "/permission-test" : "/login");
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
去权限测试页
|
|
101
|
+
</Button>
|
|
102
|
+
</Space>
|
|
103
|
+
|
|
104
|
+
<Space size={[8, 8]} wrap>
|
|
105
|
+
<Tag color={authState.isAuthenticated ? "success" : "default"}>
|
|
106
|
+
当前状态:{authState.isAuthenticated ? "已登录" : "未登录"}
|
|
107
|
+
</Tag>
|
|
108
|
+
<Tag color="cyan">最近一次登录:{lastLoginUser}</Tag>
|
|
109
|
+
</Space>
|
|
110
|
+
</Space>
|
|
111
|
+
</Card>
|
|
112
|
+
|
|
113
|
+
<Row gutter={[16, 16]}>
|
|
114
|
+
{highlights.map((item) => (
|
|
115
|
+
<Col key={item.title} md={12} xl={6} xs={24}>
|
|
116
|
+
<Card className="surface-card" variant="borderless">
|
|
117
|
+
<Space direction="vertical" size={12}>
|
|
118
|
+
<div className="feature-icon">{item.icon}</div>
|
|
119
|
+
<Title level={4}>{item.title}</Title>
|
|
120
|
+
<Paragraph className="compact-text">{item.description}</Paragraph>
|
|
121
|
+
</Space>
|
|
122
|
+
</Card>
|
|
123
|
+
</Col>
|
|
124
|
+
))}
|
|
125
|
+
</Row>
|
|
126
|
+
</Space>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { LoginOutlined, UserOutlined } from "@ant-design/icons";
|
|
2
|
+
import { Alert, Button, Card, Form, Input, List, Space, Typography, message } from "antd";
|
|
3
|
+
import { startTransition, useEffect, useState } from "react";
|
|
4
|
+
import { useNavigate } from "react-router-dom";
|
|
5
|
+
import { authStore } from "@/features/auth/auth-store";
|
|
6
|
+
import { useAuth } from "@/features/auth/use-auth";
|
|
7
|
+
import type { LoginPayload } from "@/features/auth/auth-types";
|
|
8
|
+
|
|
9
|
+
const { Paragraph, Text, Title } = Typography;
|
|
10
|
+
|
|
11
|
+
const demoAccounts = [
|
|
12
|
+
"admin / admin123",
|
|
13
|
+
"editor / editor123",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export default function LoginPage() {
|
|
17
|
+
const navigate = useNavigate();
|
|
18
|
+
const authState = useAuth();
|
|
19
|
+
const [form] = Form.useForm<LoginPayload>();
|
|
20
|
+
const [errorMessage, setErrorMessage] = useState<string>("");
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!authState.ready || !authState.isAuthenticated) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
startTransition(() => {
|
|
28
|
+
void navigate("/dashboard");
|
|
29
|
+
});
|
|
30
|
+
}, [authState.isAuthenticated, authState.ready, navigate]);
|
|
31
|
+
|
|
32
|
+
const handleSubmit = async (values: LoginPayload) => {
|
|
33
|
+
setErrorMessage("");
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await authStore.login(values);
|
|
37
|
+
message.success("模拟登录成功");
|
|
38
|
+
startTransition(() => {
|
|
39
|
+
void navigate("/dashboard");
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
setErrorMessage(error instanceof Error ? error.message : "登录失败");
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<main className="login-page">
|
|
48
|
+
<div className="login-page__backdrop" />
|
|
49
|
+
<Card className="login-card" variant="borderless">
|
|
50
|
+
<Space direction="vertical" size={20} style={{ width: "100%" }}>
|
|
51
|
+
<Space align="center" size={12}>
|
|
52
|
+
<div className="login-card__badge">
|
|
53
|
+
<LoginOutlined />
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<Title level={2}>模拟登录</Title>
|
|
57
|
+
<Paragraph className="compact-text">
|
|
58
|
+
账号信息不会发真实网络请求,而是通过
|
|
59
|
+
<Text code> @vlian/request </Text>
|
|
60
|
+
注册的本地 <Text code>kv</Text> 适配器读写浏览器缓存。
|
|
61
|
+
</Paragraph>
|
|
62
|
+
</div>
|
|
63
|
+
</Space>
|
|
64
|
+
|
|
65
|
+
{errorMessage ? <Alert message={errorMessage} showIcon type="error" /> : null}
|
|
66
|
+
|
|
67
|
+
<Form
|
|
68
|
+
form={form}
|
|
69
|
+
initialValues={{
|
|
70
|
+
username: "admin",
|
|
71
|
+
password: "admin123",
|
|
72
|
+
}}
|
|
73
|
+
layout="vertical"
|
|
74
|
+
onFinish={(values: LoginPayload) => {
|
|
75
|
+
void handleSubmit(values);
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<Form.Item
|
|
79
|
+
label="账号"
|
|
80
|
+
name="username"
|
|
81
|
+
rules={[
|
|
82
|
+
{
|
|
83
|
+
required: true,
|
|
84
|
+
message: "请输入账号",
|
|
85
|
+
},
|
|
86
|
+
]}
|
|
87
|
+
>
|
|
88
|
+
<Input placeholder="admin" prefix={<UserOutlined />} />
|
|
89
|
+
</Form.Item>
|
|
90
|
+
|
|
91
|
+
<Form.Item
|
|
92
|
+
label="密码"
|
|
93
|
+
name="password"
|
|
94
|
+
rules={[
|
|
95
|
+
{
|
|
96
|
+
required: true,
|
|
97
|
+
message: "请输入密码",
|
|
98
|
+
},
|
|
99
|
+
]}
|
|
100
|
+
>
|
|
101
|
+
<Input.Password placeholder="admin123" />
|
|
102
|
+
</Form.Item>
|
|
103
|
+
|
|
104
|
+
<Button
|
|
105
|
+
block
|
|
106
|
+
htmlType="submit"
|
|
107
|
+
icon={<LoginOutlined />}
|
|
108
|
+
loading={authState.pending}
|
|
109
|
+
size="large"
|
|
110
|
+
type="primary"
|
|
111
|
+
>
|
|
112
|
+
登录并进入仪表盘
|
|
113
|
+
</Button>
|
|
114
|
+
</Form>
|
|
115
|
+
|
|
116
|
+
<Card className="login-helper" size="small">
|
|
117
|
+
<Title level={5}>可用测试账号</Title>
|
|
118
|
+
<List
|
|
119
|
+
dataSource={demoAccounts}
|
|
120
|
+
renderItem={(item: string) => <List.Item>{item}</List.Item>}
|
|
121
|
+
size="small"
|
|
122
|
+
/>
|
|
123
|
+
</Card>
|
|
124
|
+
</Space>
|
|
125
|
+
</Card>
|
|
126
|
+
</main>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ArrowRightOutlined, LockOutlined } from "@ant-design/icons";
|
|
2
|
+
import { Alert, Button, Card, Space, Tag, Typography } from "antd";
|
|
3
|
+
import { useNavigate } from "react-router-dom";
|
|
4
|
+
import { useAuth } from "@/features/auth/use-auth";
|
|
5
|
+
|
|
6
|
+
const { Paragraph, Title } = Typography;
|
|
7
|
+
|
|
8
|
+
export default function PermissionTestPage() {
|
|
9
|
+
const navigate = useNavigate();
|
|
10
|
+
const authState = useAuth();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Space className="page-stack" direction="vertical" size={24}>
|
|
14
|
+
<Card className="page-hero" variant="borderless">
|
|
15
|
+
<Tag color="volcano" icon={<LockOutlined />}>
|
|
16
|
+
权限模拟
|
|
17
|
+
</Tag>
|
|
18
|
+
<Title level={2}>没有权限测试页</Title>
|
|
19
|
+
<Paragraph>
|
|
20
|
+
当前页本身需要登录,但不会要求额外权限。点击下方按钮后会跳到一个需要
|
|
21
|
+
<strong> system:restricted </strong>
|
|
22
|
+
权限的资源,你的测试账号默认不具备这个权限,因此会被路由层重定向到
|
|
23
|
+
403 页面。
|
|
24
|
+
</Paragraph>
|
|
25
|
+
</Card>
|
|
26
|
+
|
|
27
|
+
<Card className="surface-card" title="当前账号权限" variant="borderless">
|
|
28
|
+
<Space size={[8, 8]} wrap>
|
|
29
|
+
{authState.permissions.map((permission) => (
|
|
30
|
+
<Tag key={permission}>{permission}</Tag>
|
|
31
|
+
))}
|
|
32
|
+
</Space>
|
|
33
|
+
</Card>
|
|
34
|
+
|
|
35
|
+
<Alert
|
|
36
|
+
message="模拟目标"
|
|
37
|
+
description="受限资源路由是静态注册的,但通过 authorization.resolve 检查权限,并在失败时跳转到 /403。"
|
|
38
|
+
showIcon
|
|
39
|
+
type="warning"
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<Button
|
|
43
|
+
icon={<ArrowRightOutlined />}
|
|
44
|
+
size="large"
|
|
45
|
+
type="primary"
|
|
46
|
+
onClick={() => {
|
|
47
|
+
void navigate("/restricted-demo");
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
访问受限资源
|
|
51
|
+
</Button>
|
|
52
|
+
</Space>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Card, Typography } from "antd";
|
|
2
|
+
|
|
3
|
+
const { Paragraph, Title } = Typography;
|
|
4
|
+
|
|
5
|
+
export default function RestrictedDemoPage() {
|
|
6
|
+
return (
|
|
7
|
+
<Card className="surface-card" variant="borderless">
|
|
8
|
+
<Title level={3}>受限资源</Title>
|
|
9
|
+
<Paragraph>
|
|
10
|
+
如果你能看到这个页面,说明当前账号已经被赋予了
|
|
11
|
+
<strong> system:restricted </strong>
|
|
12
|
+
权限。
|
|
13
|
+
</Paragraph>
|
|
14
|
+
</Card>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getKernel } from "@vlian/framework/kernel";
|
|
2
|
+
|
|
3
|
+
export const APP_INSTANCE_ID = "secra-antd-adapter-template";
|
|
4
|
+
|
|
5
|
+
export const getAppKernel = () => getKernel(APP_INSTANCE_ID);
|
|
6
|
+
|
|
7
|
+
export const getAppCache = () => getAppKernel().getCacheManager().cache;
|
|
8
|
+
|
|
9
|
+
export const getAppLogger = () => getAppKernel().getLoggerManager();
|
|
10
|
+
|