create-secra 0.1.5 → 0.1.11

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 (64) hide show
  1. package/README.md +6 -0
  2. package/README.zh-CN.md +6 -0
  3. package/antd-adapter-template/apps/core/index.html +13 -0
  4. package/antd-adapter-template/apps/core/package.json +18 -0
  5. package/antd-adapter-template/apps/core/public/favicon.ico +1 -0
  6. package/antd-adapter-template/apps/core/public/favicon.svg +1 -0
  7. package/antd-adapter-template/apps/core/public/logo.svg +1 -0
  8. package/antd-adapter-template/apps/core/src/api/auth.ts +49 -0
  9. package/antd-adapter-template/apps/core/src/assets/react.svg +1 -0
  10. package/antd-adapter-template/apps/core/src/components/AntdGlobalProvider.tsx +87 -0
  11. package/antd-adapter-template/apps/core/src/components/AntdRootLayout.tsx +10 -0
  12. package/antd-adapter-template/apps/core/src/components/layout.tsx +387 -0
  13. package/antd-adapter-template/apps/core/src/guards/auth-route-guard.ts +45 -0
  14. package/antd-adapter-template/apps/core/src/main.tsx +65 -0
  15. package/antd-adapter-template/apps/core/src/pages/auth/components/account-login-fields.tsx +60 -0
  16. package/antd-adapter-template/apps/core/src/pages/auth/components/phone-login-fields.tsx +60 -0
  17. package/antd-adapter-template/apps/core/src/pages/auth/login.tsx +169 -0
  18. package/antd-adapter-template/apps/core/src/pages/index.tsx +156 -0
  19. package/antd-adapter-template/apps/core/src/router.ts +42 -0
  20. package/antd-adapter-template/apps/core/src/shims/use-sync-external-store-shim.ts +3 -0
  21. package/antd-adapter-template/apps/core/src/theme/theme.css +48 -0
  22. package/antd-adapter-template/apps/core/src/types/crypto-js.d.ts +5 -0
  23. package/antd-adapter-template/apps/core/src/utils/index.ts +12 -0
  24. package/antd-adapter-template/apps/core/src/utils/md5.ts +6 -0
  25. package/antd-adapter-template/apps/core/tsconfig.app.json +11 -0
  26. package/antd-adapter-template/apps/core/tsconfig.json +13 -0
  27. package/antd-adapter-template/apps/core/tsconfig.node.json +7 -0
  28. package/antd-adapter-template/apps/core/vite.config.ts +118 -0
  29. package/antd-adapter-template/eslint.config.js +23 -0
  30. package/antd-adapter-template/package.json +63 -0
  31. package/antd-adapter-template/packages/sdk/.swcrc +18 -0
  32. package/antd-adapter-template/packages/sdk/package.json +52 -0
  33. package/antd-adapter-template/packages/sdk/src/build/index.ts +28 -0
  34. package/antd-adapter-template/packages/sdk/src/build/plugins/auto-import.ts +46 -0
  35. package/antd-adapter-template/packages/sdk/src/build/plugins/bundle-analyzer.ts +33 -0
  36. package/antd-adapter-template/packages/sdk/src/build/plugins/remove-console.ts +23 -0
  37. package/antd-adapter-template/packages/sdk/src/build/plugins/unocss.ts +202 -0
  38. package/antd-adapter-template/packages/sdk/src/build/plugins/unplugin-icon.ts +43 -0
  39. package/antd-adapter-template/packages/sdk/src/components/i18n-switch-dropdown.tsx +139 -0
  40. package/antd-adapter-template/packages/sdk/src/components/index.ts +2 -0
  41. package/antd-adapter-template/packages/sdk/src/components/theme-switch-dropdown.tsx +131 -0
  42. package/antd-adapter-template/packages/sdk/src/hooks/auth/core.ts +101 -0
  43. package/antd-adapter-template/packages/sdk/src/hooks/auth/index.ts +139 -0
  44. package/antd-adapter-template/packages/sdk/src/hooks/auth/with-auth.tsx +41 -0
  45. package/antd-adapter-template/packages/sdk/src/hooks/index.ts +1 -0
  46. package/antd-adapter-template/packages/sdk/src/i18n/index.ts +150 -0
  47. package/antd-adapter-template/packages/sdk/src/index.ts +11 -0
  48. package/antd-adapter-template/packages/sdk/src/request/index.ts +436 -0
  49. package/antd-adapter-template/packages/sdk/src/storage/README.md +30 -0
  50. package/antd-adapter-template/packages/sdk/src/storage/index.ts +57 -0
  51. package/antd-adapter-template/packages/sdk/src/styles/reset.css +111 -0
  52. package/antd-adapter-template/packages/sdk/src/theme/index.ts +466 -0
  53. package/antd-adapter-template/packages/sdk/tsconfig.json +16 -0
  54. package/antd-adapter-template/pnpm-workspace.yaml +3 -0
  55. package/antd-adapter-template/tsconfig.app.json +29 -0
  56. package/antd-adapter-template/tsconfig.json +7 -0
  57. package/antd-adapter-template/tsconfig.node.json +27 -0
  58. package/antd-adapter-template/turbo.json +17 -0
  59. package/bin/index.mjs +165 -33
  60. package/package.json +3 -2
  61. package/template/apps/core/src/main.tsx +11 -18
  62. package/template/apps/core/src/router.ts +5 -1
  63. package/template/package.json +1 -1
  64. 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,3 @@
1
+ import { useSyncExternalStore as reactUseSyncExternalStore } from 'react';
2
+
3
+ export const useSyncExternalStore = reactUseSyncExternalStore;
@@ -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,5 @@
1
+ declare module "crypto-js/md5" {
2
+ const md5: (input: string) => { toString: () => string };
3
+ export default md5;
4
+ }
5
+
@@ -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
+
@@ -0,0 +1,6 @@
1
+ import md5Impl from "crypto-js/md5";
2
+
3
+ export const md5 = (input: string): string => {
4
+ return md5Impl(input).toString();
5
+ };
6
+
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.app.json",
3
+ "compilerOptions": {
4
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5
+ "baseUrl": ".",
6
+ "paths": {
7
+ "@/*": ["./src/*"]
8
+ }
9
+ },
10
+ "include": ["src"]
11
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "@/*": ["./src/*"]
6
+ }
7
+ },
8
+ "files": [],
9
+ "references": [
10
+ { "path": "./tsconfig.app.json" },
11
+ { "path": "./tsconfig.node.json" }
12
+ ]
13
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.node.json",
3
+ "compilerOptions": {
4
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
5
+ },
6
+ "include": ["vite.config.ts"]
7
+ }