create-secra 0.1.5 → 0.1.7
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/README.md +6 -0
- package/README.zh-CN.md +6 -0
- package/antd-adapter-template/apps/core/index.html +13 -0
- package/antd-adapter-template/apps/core/package.json +18 -0
- package/antd-adapter-template/apps/core/public/favicon.ico +1 -0
- package/antd-adapter-template/apps/core/public/favicon.svg +1 -0
- package/antd-adapter-template/apps/core/public/logo.svg +1 -0
- package/antd-adapter-template/apps/core/src/api/auth.ts +49 -0
- package/antd-adapter-template/apps/core/src/assets/react.svg +1 -0
- package/antd-adapter-template/apps/core/src/components/AntdGlobalProvider.tsx +87 -0
- package/antd-adapter-template/apps/core/src/components/AntdRootLayout.tsx +10 -0
- package/antd-adapter-template/apps/core/src/components/layout.tsx +387 -0
- package/antd-adapter-template/apps/core/src/guards/auth-route-guard.ts +45 -0
- package/antd-adapter-template/apps/core/src/main.tsx +65 -0
- package/antd-adapter-template/apps/core/src/pages/auth/components/account-login-fields.tsx +60 -0
- package/antd-adapter-template/apps/core/src/pages/auth/components/phone-login-fields.tsx +60 -0
- package/antd-adapter-template/apps/core/src/pages/auth/login.tsx +169 -0
- package/antd-adapter-template/apps/core/src/pages/index.tsx +156 -0
- package/antd-adapter-template/apps/core/src/router.ts +42 -0
- package/antd-adapter-template/apps/core/src/shims/use-sync-external-store-shim.ts +3 -0
- package/antd-adapter-template/apps/core/src/theme/theme.css +48 -0
- package/antd-adapter-template/apps/core/src/types/crypto-js.d.ts +5 -0
- package/antd-adapter-template/apps/core/src/utils/index.ts +12 -0
- package/antd-adapter-template/apps/core/src/utils/md5.ts +6 -0
- package/antd-adapter-template/apps/core/tsconfig.app.json +11 -0
- package/antd-adapter-template/apps/core/tsconfig.json +13 -0
- package/antd-adapter-template/apps/core/tsconfig.node.json +7 -0
- package/antd-adapter-template/apps/core/vite.config.ts +118 -0
- package/antd-adapter-template/eslint.config.js +23 -0
- package/antd-adapter-template/package.json +63 -0
- package/antd-adapter-template/packages/sdk/.swcrc +18 -0
- package/antd-adapter-template/packages/sdk/package.json +52 -0
- package/antd-adapter-template/packages/sdk/src/build/index.ts +28 -0
- package/antd-adapter-template/packages/sdk/src/build/plugins/auto-import.ts +46 -0
- package/antd-adapter-template/packages/sdk/src/build/plugins/bundle-analyzer.ts +33 -0
- package/antd-adapter-template/packages/sdk/src/build/plugins/remove-console.ts +23 -0
- package/antd-adapter-template/packages/sdk/src/build/plugins/unocss.ts +202 -0
- package/antd-adapter-template/packages/sdk/src/build/plugins/unplugin-icon.ts +43 -0
- package/antd-adapter-template/packages/sdk/src/components/i18n-switch-dropdown.tsx +139 -0
- package/antd-adapter-template/packages/sdk/src/components/index.ts +2 -0
- package/antd-adapter-template/packages/sdk/src/components/theme-switch-dropdown.tsx +131 -0
- package/antd-adapter-template/packages/sdk/src/hooks/auth/core.ts +101 -0
- package/antd-adapter-template/packages/sdk/src/hooks/auth/index.ts +139 -0
- package/antd-adapter-template/packages/sdk/src/hooks/auth/with-auth.tsx +41 -0
- package/antd-adapter-template/packages/sdk/src/hooks/index.ts +1 -0
- package/antd-adapter-template/packages/sdk/src/i18n/index.ts +150 -0
- package/antd-adapter-template/packages/sdk/src/index.ts +11 -0
- package/antd-adapter-template/packages/sdk/src/request/index.ts +436 -0
- package/antd-adapter-template/packages/sdk/src/storage/README.md +30 -0
- package/antd-adapter-template/packages/sdk/src/storage/index.ts +57 -0
- package/antd-adapter-template/packages/sdk/src/styles/reset.css +111 -0
- package/antd-adapter-template/packages/sdk/src/theme/index.ts +466 -0
- package/antd-adapter-template/packages/sdk/tsconfig.json +16 -0
- package/antd-adapter-template/pnpm-workspace.yaml +3 -0
- package/antd-adapter-template/tsconfig.app.json +29 -0
- package/antd-adapter-template/tsconfig.json +7 -0
- package/antd-adapter-template/tsconfig.node.json +27 -0
- package/antd-adapter-template/turbo.json +17 -0
- package/bin/index.mjs +165 -33
- package/package.json +3 -2
- package/template/apps/core/src/main.tsx +11 -18
- package/template/apps/core/src/router.ts +5 -1
- package/template/package.json +1 -1
- package/template/packages/sdk/src/build/plugins/unocss.ts +3 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import "@ant-design/v5-patch-for-react-19";
|
|
2
|
+
import "@vlian/sdk/reset.css";
|
|
3
|
+
import "uno.css";
|
|
4
|
+
import "./theme/theme.css";
|
|
5
|
+
import { PreloadStrategy, getRoutePreloader, startApp } from '@vlian/framework/core';
|
|
6
|
+
import { LogLevel } from '@vlian/framework/utils';
|
|
7
|
+
import { ReduxAdapter } from '@vlian/framework/state';
|
|
8
|
+
import { cache, getSdkI18nLocales, setSdkLang, type SdkLang } from "@vlian/sdk";
|
|
9
|
+
import { authRouteGuard } from "./guards/auth-route-guard";
|
|
10
|
+
import { getRoutes } from './router';
|
|
11
|
+
|
|
12
|
+
const LANG_STORAGE_KEY = "sdk-lang";
|
|
13
|
+
|
|
14
|
+
const isSdkLang = (lang: unknown): lang is SdkLang => {
|
|
15
|
+
return lang === "zh-CN" || lang === "en-US";
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const stateAdapter = new ReduxAdapter({
|
|
19
|
+
devMode: import.meta.env.DEV,
|
|
20
|
+
storeConfig: {
|
|
21
|
+
devTools: import.meta.env.DEV,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Disable framework route preloader to avoid pulling non-current heavy routes on first paint.
|
|
26
|
+
getRoutePreloader({
|
|
27
|
+
strategy: PreloadStrategy.NONE,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
void startApp({
|
|
31
|
+
showSplashScreen: 'never', // 自动判断是否显示启动页
|
|
32
|
+
loggerLevel: LogLevel.DEBUG,
|
|
33
|
+
globalProvider: () => import('./components/AntdGlobalProvider'),
|
|
34
|
+
locale: getSdkI18nLocales(),
|
|
35
|
+
theme: {
|
|
36
|
+
mode: 'system',
|
|
37
|
+
primaryColor: '#1677ff',
|
|
38
|
+
},
|
|
39
|
+
configStrategy: {
|
|
40
|
+
//加载关键配置 这些配置必须在应用渲染前加载完成
|
|
41
|
+
loadCriticalConfig: async () => {
|
|
42
|
+
return {}
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
lifecycle: {
|
|
46
|
+
afterInitialization: async () => {
|
|
47
|
+
const savedLang = await cache.localstorage.get<SdkLang>(LANG_STORAGE_KEY);
|
|
48
|
+
setSdkLang(isSdkLang(savedLang) ? savedLang : "zh-CN");
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
router: {
|
|
52
|
+
enabled: true,
|
|
53
|
+
routes: getRoutes,
|
|
54
|
+
mode: 'browser',
|
|
55
|
+
hooks: {
|
|
56
|
+
beforeEach: authRouteGuard,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
stateManager: {
|
|
60
|
+
defaultAdapter: stateAdapter,
|
|
61
|
+
enableRegistry: true,
|
|
62
|
+
defaultScope: 'core-app',
|
|
63
|
+
devMode: import.meta.env.DEV,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { LockOutlined, UserOutlined } from '@ant-design/icons';
|
|
2
|
+
import { ProFormText } from '@ant-design/pro-components';
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
|
|
5
|
+
export function AccountLoginFields() {
|
|
6
|
+
const { t } = useTranslation();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<ProFormText
|
|
11
|
+
name="username"
|
|
12
|
+
fieldProps={{
|
|
13
|
+
size: 'large',
|
|
14
|
+
prefix: <UserOutlined className="prefixIcon" />,
|
|
15
|
+
}}
|
|
16
|
+
placeholder={t("secraAdmin.login.usernamePlaceholder")}
|
|
17
|
+
rules={[
|
|
18
|
+
{
|
|
19
|
+
required: true,
|
|
20
|
+
message: t("secraAdmin.login.usernameRequired"),
|
|
21
|
+
},
|
|
22
|
+
]}
|
|
23
|
+
/>
|
|
24
|
+
<ProFormText.Password
|
|
25
|
+
name="password"
|
|
26
|
+
fieldProps={{
|
|
27
|
+
size: 'large',
|
|
28
|
+
prefix: <LockOutlined className="prefixIcon" />,
|
|
29
|
+
strengthText: t("secraAdmin.login.passwordStrengthText"),
|
|
30
|
+
statusRender: (value) => {
|
|
31
|
+
const getStatus = () => {
|
|
32
|
+
if (value && value.length > 12) {
|
|
33
|
+
return 'ok';
|
|
34
|
+
}
|
|
35
|
+
if (value && value.length > 6) {
|
|
36
|
+
return 'pass';
|
|
37
|
+
}
|
|
38
|
+
return 'poor';
|
|
39
|
+
};
|
|
40
|
+
const status = getStatus();
|
|
41
|
+
if (status === 'pass') {
|
|
42
|
+
return <div className="text-amber-500">{t("secraAdmin.login.passwordStrengthMedium")}</div>;
|
|
43
|
+
}
|
|
44
|
+
if (status === 'ok') {
|
|
45
|
+
return <div className="text-emerald-500">{t("secraAdmin.login.passwordStrengthStrong")}</div>;
|
|
46
|
+
}
|
|
47
|
+
return <div className="text-rose-500">{t("secraAdmin.login.passwordStrengthWeak")}</div>;
|
|
48
|
+
},
|
|
49
|
+
}}
|
|
50
|
+
placeholder={t("secraAdmin.login.passwordPlaceholder")}
|
|
51
|
+
rules={[
|
|
52
|
+
{
|
|
53
|
+
required: true,
|
|
54
|
+
message: t("secraAdmin.login.passwordRequired"),
|
|
55
|
+
},
|
|
56
|
+
]}
|
|
57
|
+
/>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { LockOutlined, MobileOutlined } from '@ant-design/icons';
|
|
2
|
+
import { ProFormCaptcha, ProFormText } from '@ant-design/pro-components';
|
|
3
|
+
import { App } from 'antd';
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
|
|
6
|
+
export function PhoneLoginFields() {
|
|
7
|
+
const { message, modal, notification } = App.useApp();
|
|
8
|
+
void modal;
|
|
9
|
+
void notification;
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<ProFormText
|
|
15
|
+
fieldProps={{
|
|
16
|
+
size: 'large',
|
|
17
|
+
prefix: <MobileOutlined className="prefixIcon" />,
|
|
18
|
+
}}
|
|
19
|
+
name="mobile"
|
|
20
|
+
placeholder={t("secraAdmin.login.mobilePlaceholder")}
|
|
21
|
+
rules={[
|
|
22
|
+
{
|
|
23
|
+
required: true,
|
|
24
|
+
message: t("secraAdmin.login.mobileRequired"),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
pattern: /^1\d{10}$/,
|
|
28
|
+
message: t("secraAdmin.login.mobileInvalid"),
|
|
29
|
+
},
|
|
30
|
+
]}
|
|
31
|
+
/>
|
|
32
|
+
<ProFormCaptcha
|
|
33
|
+
fieldProps={{
|
|
34
|
+
size: 'large',
|
|
35
|
+
prefix: <LockOutlined className="prefixIcon" />,
|
|
36
|
+
}}
|
|
37
|
+
captchaProps={{
|
|
38
|
+
size: 'large',
|
|
39
|
+
}}
|
|
40
|
+
placeholder={t("secraAdmin.login.captchaPlaceholder")}
|
|
41
|
+
captchaTextRender={(timing, count) => {
|
|
42
|
+
if (timing) {
|
|
43
|
+
return t("secraAdmin.login.captchaButtonTiming", { count });
|
|
44
|
+
}
|
|
45
|
+
return t("secraAdmin.login.captchaButton");
|
|
46
|
+
}}
|
|
47
|
+
name="captcha"
|
|
48
|
+
rules={[
|
|
49
|
+
{
|
|
50
|
+
required: true,
|
|
51
|
+
message: t("secraAdmin.login.captchaRequired"),
|
|
52
|
+
},
|
|
53
|
+
]}
|
|
54
|
+
onGetCaptcha={async () => {
|
|
55
|
+
message.success(t("secraAdmin.login.captchaSuccess"));
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlipayCircleOutlined,
|
|
3
|
+
TaobaoCircleOutlined,
|
|
4
|
+
WeiboCircleOutlined,
|
|
5
|
+
} from '@ant-design/icons';
|
|
6
|
+
import {
|
|
7
|
+
LoginForm,
|
|
8
|
+
ProFormCheckbox,
|
|
9
|
+
} from '@ant-design/pro-components';
|
|
10
|
+
import { cache, I18nSwitchDropdown, ThemeSwitchDropdown, useThemeSwitch } from '@vlian/sdk';
|
|
11
|
+
import { setToken } from '@vlian/sdk/hooks/auth';
|
|
12
|
+
import {App, Form, Space, Tabs, Typography} from 'antd';
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
import { redirect, useNavigate } from 'react-router-dom';
|
|
15
|
+
import { useTranslation } from 'react-i18next';
|
|
16
|
+
import { login } from '../../api/auth';
|
|
17
|
+
import { md5 } from '../../utils/md5';
|
|
18
|
+
import { AccountLoginFields } from './components/account-login-fields';
|
|
19
|
+
import { PhoneLoginFields } from './components/phone-login-fields';
|
|
20
|
+
|
|
21
|
+
type LoginType = 'phone' | 'account';
|
|
22
|
+
type LoginFormValues = {
|
|
23
|
+
username?: string;
|
|
24
|
+
password?: string;
|
|
25
|
+
mobile?: string;
|
|
26
|
+
captcha?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ACCESS_TOKEN_KEY = "auth-access-token";
|
|
30
|
+
const REFRESH_TOKEN_KEY = "auth-refresh-token";
|
|
31
|
+
const USER_INFO_KEY = "auth-user-info";
|
|
32
|
+
|
|
33
|
+
export default function LoginPage() {
|
|
34
|
+
const [loginType, setLoginType] = useState<LoginType>('account');
|
|
35
|
+
const [form] = Form.useForm<LoginFormValues>();
|
|
36
|
+
const navigate = useNavigate();
|
|
37
|
+
const { message, modal, notification } = App.useApp();
|
|
38
|
+
void modal;
|
|
39
|
+
const { resolvedMode } = useThemeSwitch();
|
|
40
|
+
const { t } = useTranslation();
|
|
41
|
+
const isDark = resolvedMode === 'dark';
|
|
42
|
+
|
|
43
|
+
const onFinish = async (values: LoginFormValues): Promise<boolean> => {
|
|
44
|
+
if (loginType !== "account") {
|
|
45
|
+
message.warning(t("secraAdmin.login.phoneLoginNotReady"));
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const identifier = values.username?.trim();
|
|
49
|
+
const password = values.password ?? "";
|
|
50
|
+
if (!identifier || !password) {
|
|
51
|
+
message.warning(t("secraAdmin.login.loginFailedCheckCredentials"));
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const response = await login({
|
|
57
|
+
identifier,
|
|
58
|
+
password: md5(password),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await Promise.all([
|
|
62
|
+
setToken(response.access_token, { key: ACCESS_TOKEN_KEY }),
|
|
63
|
+
setToken(response.refresh_token, { key: REFRESH_TOKEN_KEY }),
|
|
64
|
+
cache.localstorage.set(USER_INFO_KEY, response.user),
|
|
65
|
+
]);
|
|
66
|
+
notification.success({
|
|
67
|
+
message: t("secraAdmin.login.loginSuccessTitle"),
|
|
68
|
+
description: t("secraAdmin.login.loginWelcomeUser", { username: identifier }),
|
|
69
|
+
placement: "topRight",
|
|
70
|
+
});
|
|
71
|
+
navigate("/", { replace: true });
|
|
72
|
+
return true;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const errorText = error instanceof Error ? error.message.trim() : "";
|
|
75
|
+
message.error(errorText || t("secraAdmin.login.loginFailedCheckCredentials"));
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
className={`relative min-h-screen overflow-hidden flex items-center justify-center px-4 py-6 transition-colors ${
|
|
83
|
+
isDark
|
|
84
|
+
? 'bg-[radial-gradient(circle_at_12%_20%,rgba(59,130,246,0.16)_0%,transparent_38%),radial-gradient(circle_at_85%_80%,rgba(16,185,129,0.12)_0%,transparent_35%),linear-gradient(135deg,#020617_0%,#0f172a_45%,#0b1225_100%)]'
|
|
85
|
+
: 'bg-[radial-gradient(circle_at_12%_20%,rgba(59,130,246,0.2)_0%,transparent_38%),radial-gradient(circle_at_85%_80%,rgba(16,185,129,0.16)_0%,transparent_35%),linear-gradient(135deg,#f8fafc_0%,#f1f5f9_45%,#e0f2fe_100%)]'
|
|
86
|
+
}`}
|
|
87
|
+
>
|
|
88
|
+
<div
|
|
89
|
+
className={`absolute right-4 top-4 z-[2] flex items-center gap-1 rounded-full border px-1 py-1 backdrop-blur-md ${
|
|
90
|
+
isDark ? 'border-slate-700 bg-slate-900/70' : 'border-slate-200 bg-white/75'
|
|
91
|
+
}`}
|
|
92
|
+
>
|
|
93
|
+
<I18nSwitchDropdown storageType="localstorage" storageKey="sdk-lang" />
|
|
94
|
+
<ThemeSwitchDropdown storageType="localstorage" storageKey="theme-mode" />
|
|
95
|
+
</div>
|
|
96
|
+
<div className={`absolute top-[-120px] right-[-80px] h-[320px] w-[320px] rounded-full blur-[22px] ${isDark ? 'bg-blue-500/15' : 'bg-blue-400/20'}`} />
|
|
97
|
+
<div className={`absolute bottom-[-140px] left-[-80px] h-[280px] w-[280px] rounded-full blur-[18px] ${isDark ? 'bg-emerald-500/15' : 'bg-emerald-400/20'}`} />
|
|
98
|
+
<div
|
|
99
|
+
className={`relative z-[1] w-full max-w-[460px] rounded-3xl border px-[18px] py-[22px] backdrop-blur-md transition-colors ${
|
|
100
|
+
isDark
|
|
101
|
+
? 'border-slate-700/90 bg-slate-900/78 shadow-[0_24px_56px_rgba(2,6,23,0.5)]'
|
|
102
|
+
: 'border-slate-200/90 bg-white/88 shadow-[0_24px_56px_rgba(15,23,42,0.14)]'
|
|
103
|
+
}`}
|
|
104
|
+
>
|
|
105
|
+
<LoginForm
|
|
106
|
+
form={form}
|
|
107
|
+
preserve={false}
|
|
108
|
+
logo="/logo.svg"
|
|
109
|
+
title={<Typography.Title level={3} className="!mb-0">Secra Admin</Typography.Title>}
|
|
110
|
+
subTitle={t("secraAdmin.login.subtitle")}
|
|
111
|
+
onFinish={onFinish}
|
|
112
|
+
actions={
|
|
113
|
+
<Space>
|
|
114
|
+
<Typography.Text>{t("secraAdmin.login.otherLoginMethods")}</Typography.Text>
|
|
115
|
+
<Typography.Text><AlipayCircleOutlined className="ml-4 text-2xl align-middle cursor-pointer transition-colors hover:text-blue-500" /></Typography.Text>
|
|
116
|
+
<Typography.Text><TaobaoCircleOutlined className="ml-4 text-2xl align-middle cursor-pointer transition-colors hover:text-orange-500" /></Typography.Text>
|
|
117
|
+
<Typography.Text><WeiboCircleOutlined className="ml-4 text-2xl align-middle cursor-pointer transition-colors hover:text-rose-500" /></Typography.Text>
|
|
118
|
+
</Space>
|
|
119
|
+
}
|
|
120
|
+
>
|
|
121
|
+
<Tabs
|
|
122
|
+
centered
|
|
123
|
+
activeKey={loginType}
|
|
124
|
+
onChange={(activeKey) => {
|
|
125
|
+
const nextType = activeKey as LoginType;
|
|
126
|
+
setLoginType(nextType);
|
|
127
|
+
if (nextType === "account") {
|
|
128
|
+
form.resetFields(["mobile", "captcha"]);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
form.resetFields(["username", "password"]);
|
|
132
|
+
}}
|
|
133
|
+
items={[
|
|
134
|
+
{ key: 'account', label: t("secraAdmin.login.accountTab") },
|
|
135
|
+
{ key: 'phone', label: t("secraAdmin.login.phoneTab") },
|
|
136
|
+
]}
|
|
137
|
+
/>
|
|
138
|
+
{loginType === 'account' && (
|
|
139
|
+
<AccountLoginFields />
|
|
140
|
+
)}
|
|
141
|
+
{loginType === 'phone' && (
|
|
142
|
+
<PhoneLoginFields />
|
|
143
|
+
)}
|
|
144
|
+
<div className="mb-6">
|
|
145
|
+
<ProFormCheckbox noStyle name="autoLogin">
|
|
146
|
+
{t("secraAdmin.login.autoLogin")}
|
|
147
|
+
</ProFormCheckbox>
|
|
148
|
+
<a className="float-right">{t("secraAdmin.login.forgotPassword")}</a>
|
|
149
|
+
</div>
|
|
150
|
+
</LoginForm>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 进入之前处理 如果有登录则跳转到首页
|
|
157
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
158
|
+
export async function loader() {
|
|
159
|
+
const [accessToken, refreshToken] = await Promise.all([
|
|
160
|
+
cache.localstorage.get<string>(ACCESS_TOKEN_KEY),
|
|
161
|
+
cache.localstorage.get<string>(REFRESH_TOKEN_KEY),
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
if (accessToken || refreshToken) {
|
|
165
|
+
return redirect('/');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useMemo, type CSSProperties } from "react";
|
|
2
|
+
import { Button as AntdButton, Input, Space, Typography } from "antd";
|
|
3
|
+
import { I18nSwitchDropdown, ThemeSwitchDropdown, useUnifiedThemePreset, type UnifiedThemePreset } from "@vlian/sdk";
|
|
4
|
+
import { withAuth } from "@vlian/sdk/hooks/auth";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
6
|
+
|
|
7
|
+
const THEME_PRESETS: UnifiedThemePreset[] = [
|
|
8
|
+
{
|
|
9
|
+
key: "ocean",
|
|
10
|
+
label: "Ocean",
|
|
11
|
+
primaryColor: "#1677ff",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
key: "emerald",
|
|
15
|
+
label: "Emerald",
|
|
16
|
+
primaryColor: "#10b981",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
key: "rose",
|
|
20
|
+
label: "Rose",
|
|
21
|
+
primaryColor: "#e11d48",
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const Home = () => {
|
|
26
|
+
const { t } = useTranslation();
|
|
27
|
+
const { theme, mode, resolvedMode, setTheme, presetKey, activePreset, applyPreset, setUnifiedPrimaryColor } = useUnifiedThemePreset(THEME_PRESETS, {
|
|
28
|
+
defaultPresetKey: THEME_PRESETS[0].key,
|
|
29
|
+
borderRadius: 8,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const palette = useMemo(
|
|
33
|
+
() =>
|
|
34
|
+
resolvedMode === "dark"
|
|
35
|
+
? {
|
|
36
|
+
text: "#e2e8f0",
|
|
37
|
+
textMuted: "#94a3b8",
|
|
38
|
+
background: "linear-gradient(180deg, #0f172a 0%, #020617 100%)",
|
|
39
|
+
cardBg: "#0b1225",
|
|
40
|
+
cardBorder: "#1f2a44",
|
|
41
|
+
}
|
|
42
|
+
: {
|
|
43
|
+
text: "#0f172a",
|
|
44
|
+
textMuted: "#475569",
|
|
45
|
+
background: "linear-gradient(180deg, #f8fafc 0%, #ffffff 100%)",
|
|
46
|
+
cardBg: "#ffffff",
|
|
47
|
+
cardBorder: "#e5e7eb",
|
|
48
|
+
},
|
|
49
|
+
[resolvedMode],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const cardStyle: CSSProperties = useMemo(
|
|
53
|
+
() => ({
|
|
54
|
+
border: `1px solid ${palette.cardBorder}`,
|
|
55
|
+
borderRadius: 12,
|
|
56
|
+
padding: 16,
|
|
57
|
+
background: palette.cardBg,
|
|
58
|
+
boxShadow: resolvedMode === "dark" ? "0 8px 28px rgba(2, 6, 23, 0.5)" : "0 6px 24px rgba(15, 23, 42, 0.06)",
|
|
59
|
+
}),
|
|
60
|
+
[palette.cardBg, palette.cardBorder, resolvedMode],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<main style={{ minHeight: "100vh", padding: "40px 20px", background: palette.background, color: palette.text, position: "relative" }}>
|
|
65
|
+
<div
|
|
66
|
+
style={{
|
|
67
|
+
position: "absolute",
|
|
68
|
+
right: 16,
|
|
69
|
+
top: 16,
|
|
70
|
+
zIndex: 1,
|
|
71
|
+
display: "flex",
|
|
72
|
+
alignItems: "center",
|
|
73
|
+
gap: 4,
|
|
74
|
+
borderRadius: 999,
|
|
75
|
+
border: `1px solid ${palette.cardBorder}`,
|
|
76
|
+
padding: 4,
|
|
77
|
+
background: resolvedMode === "dark" ? "rgba(15, 23, 42, 0.65)" : "rgba(255, 255, 255, 0.75)",
|
|
78
|
+
backdropFilter: "blur(8px)",
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<I18nSwitchDropdown storageType="localstorage" storageKey="sdk-lang" />
|
|
82
|
+
<ThemeSwitchDropdown storageType="localstorage" storageKey="theme-mode" />
|
|
83
|
+
</div>
|
|
84
|
+
<section style={{ maxWidth: 980, margin: "0 auto", display: "grid", gap: 14 }}>
|
|
85
|
+
<div style={cardStyle}>
|
|
86
|
+
<h1 style={{ margin: 0, fontSize: 30 }}>{t("secraAdmin.home.title")}</h1>
|
|
87
|
+
<p style={{ margin: "8px 0 0", color: palette.textMuted }}>
|
|
88
|
+
{t("secraAdmin.home.currentMode")}:<b>{mode}</b>({t("secraAdmin.home.resolvedMode")}:<b>{resolvedMode}</b>)
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div style={cardStyle}>
|
|
93
|
+
<h3 style={{ marginTop: 0 }}>{t("secraAdmin.home.modeSwitchTitle")}</h3>
|
|
94
|
+
<Space wrap>
|
|
95
|
+
{(["light", "dark", "system"] as const).map((nextMode) => (
|
|
96
|
+
<AntdButton
|
|
97
|
+
key={nextMode}
|
|
98
|
+
type={mode === nextMode ? "primary" : "default"}
|
|
99
|
+
onClick={() =>
|
|
100
|
+
setTheme((prev) => {
|
|
101
|
+
const previous = prev ?? {};
|
|
102
|
+
return { ...previous, mode: nextMode };
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
>
|
|
106
|
+
{t(`secraAdmin.home.mode.${nextMode}`)}
|
|
107
|
+
</AntdButton>
|
|
108
|
+
))}
|
|
109
|
+
</Space>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div style={cardStyle}>
|
|
113
|
+
<h3 style={{ marginTop: 0 }}>{t("secraAdmin.home.unifiedThemeTitle")}</h3>
|
|
114
|
+
<Space wrap>
|
|
115
|
+
{THEME_PRESETS.map((preset) => (
|
|
116
|
+
<AntdButton key={preset.key} type={presetKey === preset.key ? "primary" : "default"} onClick={() => applyPreset(preset.key)}>
|
|
117
|
+
{preset.label}
|
|
118
|
+
</AntdButton>
|
|
119
|
+
))}
|
|
120
|
+
<Input
|
|
121
|
+
style={{ width: 180 }}
|
|
122
|
+
value={theme.primaryColor ?? activePreset?.primaryColor ?? "#1677ff"}
|
|
123
|
+
onChange={(event) => {
|
|
124
|
+
setUnifiedPrimaryColor(event.target.value);
|
|
125
|
+
}}
|
|
126
|
+
placeholder="#1677ff"
|
|
127
|
+
/>
|
|
128
|
+
</Space>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div style={cardStyle}>
|
|
132
|
+
<h3 style={{ marginTop: 0 }}>{t("secraAdmin.home.previewTitle")}</h3>
|
|
133
|
+
<Space wrap>
|
|
134
|
+
<AntdButton type="primary">Antd Primary</AntdButton>
|
|
135
|
+
<AntdButton>Antd Default</AntdButton>
|
|
136
|
+
<AntdButton type="dashed">Antd Dashed</AntdButton>
|
|
137
|
+
<AntdButton type="text">Antd Text</AntdButton>
|
|
138
|
+
</Space>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div style={cardStyle}>
|
|
142
|
+
<h3 style={{ marginTop: 0 }}>{t("secraAdmin.home.currentUnifiedThemeTitle")}</h3>
|
|
143
|
+
<Typography.Paragraph style={{ marginTop: 10, marginBottom: 0, color: palette.textMuted }}>
|
|
144
|
+
{t("secraAdmin.home.themePreset")}:<b>{activePreset?.label ?? "-"}</b>,primaryColor:<b>{theme.primaryColor}</b>
|
|
145
|
+
</Typography.Paragraph>
|
|
146
|
+
</div>
|
|
147
|
+
</section>
|
|
148
|
+
</main>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const HomeWithAuth = withAuth(Home);
|
|
153
|
+
|
|
154
|
+
HomeWithAuth.displayName = "Home";
|
|
155
|
+
|
|
156
|
+
export default HomeWithAuth;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { RouteConfig } from "@vlian/framework/core"
|
|
2
|
+
import { loader as LoginLoader } from "./pages/auth/login";
|
|
3
|
+
|
|
4
|
+
const LoginPage = lazy(() => import("./pages/auth/login"));
|
|
5
|
+
const HomePage = lazy(() => import("./pages/index"));
|
|
6
|
+
|
|
7
|
+
export const getRoutes = async (): Promise<RouteConfig[]> => {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
path: "/auth/login",
|
|
11
|
+
name: "authLogin",
|
|
12
|
+
page: async () => ({
|
|
13
|
+
default: LoginPage,
|
|
14
|
+
loader: LoginLoader,
|
|
15
|
+
}),
|
|
16
|
+
handle: {
|
|
17
|
+
title: "登录",
|
|
18
|
+
order: 0,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
path: "/",
|
|
23
|
+
name: "home",
|
|
24
|
+
page: async () => ({
|
|
25
|
+
default: HomePage,
|
|
26
|
+
}),
|
|
27
|
+
handle: {
|
|
28
|
+
title: "首页",
|
|
29
|
+
order: 1,
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
path: "/test",
|
|
34
|
+
name: "Test",
|
|
35
|
+
layout: () => import('./components/layout.tsx'),
|
|
36
|
+
handle: {
|
|
37
|
+
title: "测试",
|
|
38
|
+
order: 2,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--background: 0 0% 100%;
|
|
3
|
+
--foreground: 222.2 84% 4.9%;
|
|
4
|
+
--card: 0 0% 100%;
|
|
5
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
6
|
+
--popover: 0 0% 100%;
|
|
7
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
8
|
+
--primary: 221.2 83.2% 53.3%;
|
|
9
|
+
--primary-foreground: 210 40% 98%;
|
|
10
|
+
--secondary: 210 40% 96.1%;
|
|
11
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
12
|
+
--muted: 210 40% 96.1%;
|
|
13
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
14
|
+
--accent: 210 40% 96.1%;
|
|
15
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
16
|
+
--destructive: 0 84.2% 60.2%;
|
|
17
|
+
--destructive-foreground: 210 40% 98%;
|
|
18
|
+
--border: 214.3 31.8% 91.4%;
|
|
19
|
+
--input: 214.3 31.8% 91.4%;
|
|
20
|
+
--ring: 221.2 83.2% 53.3%;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.dark {
|
|
24
|
+
--background: 222.2 84% 4.9%;
|
|
25
|
+
--foreground: 210 40% 98%;
|
|
26
|
+
--card: 222.2 84% 4.9%;
|
|
27
|
+
--card-foreground: 210 40% 98%;
|
|
28
|
+
--popover: 222.2 84% 4.9%;
|
|
29
|
+
--popover-foreground: 210 40% 98%;
|
|
30
|
+
--primary: 217.2 91.2% 59.8%;
|
|
31
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
32
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
33
|
+
--secondary-foreground: 210 40% 98%;
|
|
34
|
+
--muted: 217.2 32.6% 17.5%;
|
|
35
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
36
|
+
--accent: 217.2 32.6% 17.5%;
|
|
37
|
+
--accent-foreground: 210 40% 98%;
|
|
38
|
+
--destructive: 0 62.8% 30.6%;
|
|
39
|
+
--destructive-foreground: 210 40% 98%;
|
|
40
|
+
--border: 217.2 32.6% 17.5%;
|
|
41
|
+
--input: 217.2 32.6% 17.5%;
|
|
42
|
+
--ring: 224.3 76.3% 48%;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
body {
|
|
46
|
+
background-color: hsl(var(--background));
|
|
47
|
+
color: hsl(var(--foreground));
|
|
48
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type LogMethod = (...args: unknown[]) => void;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compatibility logger for framework code that imports `@/utils`.
|
|
5
|
+
*/
|
|
6
|
+
export const logger: Record<'info' | 'warn' | 'error' | 'debug', LogMethod> = {
|
|
7
|
+
info: (...args) => console.info(...args),
|
|
8
|
+
warn: (...args) => console.warn(...args),
|
|
9
|
+
error: (...args) => console.error(...args),
|
|
10
|
+
debug: (...args) => console.debug(...args),
|
|
11
|
+
};
|
|
12
|
+
|