generator-mico-cli 0.1.18
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 +84 -0
- package/bin/mico.js +316 -0
- package/generators/micro-react/ignore-list.json +8 -0
- package/generators/micro-react/index.js +158 -0
- package/generators/micro-react/templates/.commitlintrc.js +6 -0
- package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +129 -0
- package/generators/micro-react/templates/.cursor/rules/cicd-deploy.mdc +143 -0
- package/generators/micro-react/templates/.cursor/rules/coding-conventions.mdc +206 -0
- package/generators/micro-react/templates/.cursor/rules/commit-conventions.mdc +111 -0
- package/generators/micro-react/templates/.cursor/rules/development-guide.mdc +295 -0
- package/generators/micro-react/templates/.cursor/rules/layout-app.mdc +275 -0
- package/generators/micro-react/templates/.cursor/rules/micro-frontend.mdc +196 -0
- package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +128 -0
- package/generators/micro-react/templates/.cursor/rules/request-auth.mdc +220 -0
- package/generators/micro-react/templates/.cursor/rules/theme-system.mdc +206 -0
- package/generators/micro-react/templates/.editorconfig +16 -0
- package/generators/micro-react/templates/.env +3 -0
- package/generators/micro-react/templates/.eslintrc.js +30 -0
- package/generators/micro-react/templates/.husky/commit-msg +2 -0
- package/generators/micro-react/templates/.husky/pre-commit +2 -0
- package/generators/micro-react/templates/.lintstagedrc +5 -0
- package/generators/micro-react/templates/.stylelintrc.js +25 -0
- package/generators/micro-react/templates/AGENTS.md +39 -0
- package/generators/micro-react/templates/CICD/start_dev.sh +30 -0
- package/generators/micro-react/templates/CICD/start_local.sh +30 -0
- package/generators/micro-react/templates/CICD/start_prod.sh +30 -0
- package/generators/micro-react/templates/CICD/start_test.sh +30 -0
- package/generators/micro-react/templates/CICD/wangsu_fresh_dev.sh +19 -0
- package/generators/micro-react/templates/CICD/wangsu_fresh_prod.sh +19 -0
- package/generators/micro-react/templates/CICD/wangsu_fresh_test.sh +19 -0
- package/generators/micro-react/templates/CLAUDE.md +106 -0
- package/generators/micro-react/templates/README.md +84 -0
- package/generators/micro-react/templates/_gitignore +57 -0
- package/generators/micro-react/templates/_npmrc +2 -0
- package/generators/micro-react/templates/apps/layout/.env +4 -0
- package/generators/micro-react/templates/apps/layout/.eslintrc.js +10 -0
- package/generators/micro-react/templates/apps/layout/.lintstagedrc +17 -0
- package/generators/micro-react/templates/apps/layout/.prettierignore +3 -0
- package/generators/micro-react/templates/apps/layout/.prettierrc +8 -0
- package/generators/micro-react/templates/apps/layout/.stylelintrc.js +20 -0
- package/generators/micro-react/templates/apps/layout/README.md +37 -0
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +54 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.ts +37 -0
- package/generators/micro-react/templates/apps/layout/config/config.testing.ts +27 -0
- package/generators/micro-react/templates/apps/layout/config/config.ts +132 -0
- package/generators/micro-react/templates/apps/layout/config/routes.ts +13 -0
- package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +78 -0
- package/generators/micro-react/templates/apps/layout/mock/menus.json +100 -0
- package/generators/micro-react/templates/apps/layout/mock/menus.ts +11 -0
- package/generators/micro-react/templates/apps/layout/mock/user.mock.ts +20 -0
- package/generators/micro-react/templates/apps/layout/package.json +45 -0
- package/generators/micro-react/templates/apps/layout/public/font/ar-SA.js +54 -0
- package/generators/micro-react/templates/apps/layout/public/font/default.js +54 -0
- package/generators/micro-react/templates/apps/layout/src/app.tsx +123 -0
- package/generators/micro-react/templates/apps/layout/src/assets/.gitkeep +0 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/cs-auth-manager.ts +220 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +41 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/tool.ts +3 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +6 -0
- package/generators/micro-react/templates/apps/layout/src/common/constants.ts +38 -0
- package/generators/micro-react/templates/apps/layout/src/common/env.ts +73 -0
- package/generators/micro-react/templates/apps/layout/src/common/helpers.ts +69 -0
- package/generators/micro-react/templates/apps/layout/src/common/locale.ts +123 -0
- package/generators/micro-react/templates/apps/layout/src/common/logger.ts +45 -0
- package/generators/micro-react/templates/apps/layout/src/common/menu/index.ts +2 -0
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +143 -0
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +92 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +73 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +188 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +186 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +132 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/token-refresh.ts +136 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/types.ts +44 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/url-resolver.ts +75 -0
- package/generators/micro-react/templates/apps/layout/src/common/theme.ts +107 -0
- package/generators/micro-react/templates/apps/layout/src/common/types.ts +7 -0
- package/generators/micro-react/templates/apps/layout/src/common/upload/index.ts +2 -0
- package/generators/micro-react/templates/apps/layout/src/common/upload/oss.ts +401 -0
- package/generators/micro-react/templates/apps/layout/src/common/upload/types.ts +47 -0
- package/generators/micro-react/templates/apps/layout/src/common/uploadFiles.ts +35 -0
- package/generators/micro-react/templates/apps/layout/src/components/IconFont/index.tsx +25 -0
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.less +44 -0
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +121 -0
- package/generators/micro-react/templates/apps/layout/src/constants/index.ts +15 -0
- package/generators/micro-react/templates/apps/layout/src/global.less +13 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/index.ts +3 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useAuth.ts +75 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useMenu.ts +35 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useMenuState.ts +112 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useTheme.ts +124 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.less +109 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +97 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +164 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +165 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/index.less +71 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +91 -0
- package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +20 -0
- package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +19 -0
- package/generators/micro-react/templates/apps/layout/src/models/global.ts +13 -0
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.less +3 -0
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +7 -0
- package/generators/micro-react/templates/apps/layout/src/requestErrorConfig.ts +171 -0
- package/generators/micro-react/templates/apps/layout/src/services/auth.ts +37 -0
- package/generators/micro-react/templates/apps/layout/src/services/oss.ts +40 -0
- package/generators/micro-react/templates/apps/layout/src/styles/arco-override.less +78 -0
- package/generators/micro-react/templates/apps/layout/src/styles/themes/dark/custom-var.less +244 -0
- package/generators/micro-react/templates/apps/layout/src/styles/themes/normal/custom-var.less +195 -0
- package/generators/micro-react/templates/apps/layout/src/styles/variables.less +5 -0
- package/generators/micro-react/templates/apps/layout/src/utils/format.ts +4 -0
- package/generators/micro-react/templates/apps/layout/tailwind.config.js +7 -0
- package/generators/micro-react/templates/apps/layout/tailwind.css +3 -0
- package/generators/micro-react/templates/apps/layout/tsconfig.json +3 -0
- package/generators/micro-react/templates/apps/layout/typings.d.ts +1 -0
- package/generators/micro-react/templates/deployDesc.md +60 -0
- package/generators/micro-react/templates/docs/commit-message.md +98 -0
- package/generators/micro-react/templates/package.json +35 -0
- package/generators/micro-react/templates/packages/shared-styles/README.md +125 -0
- package/generators/micro-react/templates/packages/shared-styles/arco-override.less +78 -0
- package/generators/micro-react/templates/packages/shared-styles/index.less +14 -0
- package/generators/micro-react/templates/packages/shared-styles/package.json +27 -0
- package/generators/micro-react/templates/packages/shared-styles/theme-inject.less +10 -0
- package/generators/micro-react/templates/packages/shared-styles/themes/dark/custom-var.less +246 -0
- package/generators/micro-react/templates/packages/shared-styles/themes/normal/custom-var.less +195 -0
- package/generators/micro-react/templates/packages/shared-styles/variables-only.less +301 -0
- package/generators/micro-react/templates/packages/shared-styles/variables.less +363 -0
- package/generators/micro-react/templates/pnpm-workspace.yaml +9 -0
- package/generators/micro-react/templates/scripts/collect-dist.js +68 -0
- package/generators/micro-react/templates/scripts/create-umi-app.sh +61 -0
- package/generators/micro-react/templates/scripts/dev.js +133 -0
- package/generators/micro-react/templates/turbo.json +68 -0
- package/generators/subapp-react/ignore-list.json +7 -0
- package/generators/subapp-react/index.js +189 -0
- package/generators/subapp-react/templates/homepage/.env +4 -0
- package/generators/subapp-react/templates/homepage/README.md +116 -0
- package/generators/subapp-react/templates/homepage/_gitignore +9 -0
- package/generators/subapp-react/templates/homepage/config/config.dev.ts +59 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.ts +41 -0
- package/generators/subapp-react/templates/homepage/config/config.testing.ts +40 -0
- package/generators/subapp-react/templates/homepage/config/config.ts +102 -0
- package/generators/subapp-react/templates/homepage/config/routes.ts +7 -0
- package/generators/subapp-react/templates/homepage/mock/api.mock.ts +59 -0
- package/generators/subapp-react/templates/homepage/package.json +30 -0
- package/generators/subapp-react/templates/homepage/src/app.tsx +80 -0
- package/generators/subapp-react/templates/homepage/src/assets/yay.jpg +0 -0
- package/generators/subapp-react/templates/homepage/src/common/logger.ts +42 -0
- package/generators/subapp-react/templates/homepage/src/common/mainApp.ts +53 -0
- package/generators/subapp-react/templates/homepage/src/common/request.ts +49 -0
- package/generators/subapp-react/templates/homepage/src/global.less +26 -0
- package/generators/subapp-react/templates/homepage/src/pages/index.less +139 -0
- package/generators/subapp-react/templates/homepage/src/pages/index.tsx +342 -0
- package/generators/subapp-react/templates/homepage/src/styles/theme.less +6 -0
- package/generators/subapp-react/templates/homepage/tsconfig.json +3 -0
- package/generators/subapp-react/templates/homepage/typings.d.ts +17 -0
- package/lib/utils.js +165 -0
- package/package.json +31 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { getAuthInfo } from '@/common/auth/tool';
|
|
2
|
+
import type { AuthInfo } from '@/common/auth/type';
|
|
3
|
+
import { handleAuthFailureRedirect, resolveAuthToken } from '@/common/request';
|
|
4
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
interface UseAuthOptions {
|
|
7
|
+
/** 是否跳过认证检查 */
|
|
8
|
+
skipAuthCheck?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 认证管理 Hook
|
|
13
|
+
*/
|
|
14
|
+
export const useAuth = (options: UseAuthOptions = {}) => {
|
|
15
|
+
const { skipAuthCheck = false } = options;
|
|
16
|
+
const [authInfo, setAuthInfo] = useState<AuthInfo | null>(null);
|
|
17
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
|
|
20
|
+
// 检查认证状态
|
|
21
|
+
const checkAuth = useCallback(() => {
|
|
22
|
+
if (skipAuthCheck) {
|
|
23
|
+
setLoading(false);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const token = resolveAuthToken();
|
|
28
|
+
if (token) {
|
|
29
|
+
const info = getAuthInfo();
|
|
30
|
+
setAuthInfo(info);
|
|
31
|
+
setIsAuthenticated(true);
|
|
32
|
+
} else {
|
|
33
|
+
setIsAuthenticated(false);
|
|
34
|
+
}
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}, [skipAuthCheck]);
|
|
37
|
+
|
|
38
|
+
// 初始化时检查认证
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
checkAuth();
|
|
41
|
+
}, [checkAuth]);
|
|
42
|
+
|
|
43
|
+
// 登出
|
|
44
|
+
const logout = useCallback(() => {
|
|
45
|
+
// 清除本地存储的认证信息
|
|
46
|
+
localStorage.removeItem('authtoken');
|
|
47
|
+
localStorage.removeItem('refreshtoken');
|
|
48
|
+
localStorage.removeItem('uid');
|
|
49
|
+
localStorage.removeItem('nickname');
|
|
50
|
+
localStorage.removeItem('avatar');
|
|
51
|
+
|
|
52
|
+
setAuthInfo(null);
|
|
53
|
+
setIsAuthenticated(false);
|
|
54
|
+
|
|
55
|
+
// 跳转到登录页
|
|
56
|
+
handleAuthFailureRedirect();
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
// 刷新认证信息
|
|
60
|
+
const refreshAuthInfo = useCallback(() => {
|
|
61
|
+
const info = getAuthInfo();
|
|
62
|
+
setAuthInfo(info);
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
authInfo,
|
|
67
|
+
isAuthenticated,
|
|
68
|
+
loading,
|
|
69
|
+
logout,
|
|
70
|
+
refreshAuthInfo,
|
|
71
|
+
checkAuth,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default useAuth;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MenuItem, ParsedMenuItem, ParsedRoute } from '@/common/menu';
|
|
2
|
+
import { extractRoutes, getWindowMenus, parseMenuItems } from '@/common/menu';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 菜单管理 Hook
|
|
7
|
+
*/
|
|
8
|
+
export const useMenu = () => {
|
|
9
|
+
// 获取原始菜单数据
|
|
10
|
+
const rawMenus = useMemo<MenuItem[]>(() => {
|
|
11
|
+
return getWindowMenus();
|
|
12
|
+
}, []);
|
|
13
|
+
|
|
14
|
+
// 解析后的菜单项(用于渲染菜单组件)
|
|
15
|
+
const menuItems = useMemo<ParsedMenuItem[]>(() => {
|
|
16
|
+
return parseMenuItems(rawMenus);
|
|
17
|
+
}, [rawMenus]);
|
|
18
|
+
|
|
19
|
+
// 解析后的路由配置(用于路由匹配)
|
|
20
|
+
const routes = useMemo<ParsedRoute[]>(() => {
|
|
21
|
+
return extractRoutes(rawMenus);
|
|
22
|
+
}, [rawMenus]);
|
|
23
|
+
|
|
24
|
+
// 是否有菜单数据
|
|
25
|
+
const hasMenus = rawMenus.length > 0;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
rawMenus,
|
|
29
|
+
menuItems,
|
|
30
|
+
routes,
|
|
31
|
+
hasMenus,
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default useMenu;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { ParsedMenuItem } from '@/common/menu';
|
|
2
|
+
import { findMenuKeyByPath } from '@/common/menu';
|
|
3
|
+
import { history, useLocation } from '@umijs/max';
|
|
4
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
export enum ETriggerMode {
|
|
7
|
+
CLICK_TRIGGER = 'clickTrigger',
|
|
8
|
+
HOVER_TRIGGER = 'hoverTrigger',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface UseMenuStateOptions {
|
|
12
|
+
menuItems: ParsedMenuItem[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UseMenuStateReturn {
|
|
16
|
+
selectedKeys: string[];
|
|
17
|
+
openKeys: string[];
|
|
18
|
+
collapsed: boolean;
|
|
19
|
+
triggerMode: ETriggerMode;
|
|
20
|
+
handleClickMenuItem: (key: string) => void;
|
|
21
|
+
handleCollapsed: (
|
|
22
|
+
isCollapsed: boolean,
|
|
23
|
+
type: 'clickTrigger' | 'responsive',
|
|
24
|
+
) => void;
|
|
25
|
+
setOpenKeys: (keys: string[]) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 菜单状态管理 Hook
|
|
30
|
+
* 分离菜单的状态逻辑,使组件更简洁
|
|
31
|
+
*/
|
|
32
|
+
export const useMenuState = ({
|
|
33
|
+
menuItems,
|
|
34
|
+
}: UseMenuStateOptions): UseMenuStateReturn => {
|
|
35
|
+
const location = useLocation();
|
|
36
|
+
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
|
37
|
+
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
|
38
|
+
const [collapsed, setCollapsed] = useState<boolean>(false);
|
|
39
|
+
const [triggerMode, setTriggerMode] = useState<ETriggerMode>(
|
|
40
|
+
ETriggerMode.CLICK_TRIGGER,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// 根据当前路径更新选中状态
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const result = findMenuKeyByPath(menuItems, location.pathname);
|
|
46
|
+
if (result) {
|
|
47
|
+
setSelectedKeys([result.key]);
|
|
48
|
+
if (!collapsed && result.openKeys.length > 0) {
|
|
49
|
+
setOpenKeys((prev) => [...new Set([...prev, ...result.openKeys])]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}, [location.pathname, menuItems, collapsed]);
|
|
53
|
+
|
|
54
|
+
// 查找菜单项
|
|
55
|
+
const findItem = useCallback(
|
|
56
|
+
(items: ParsedMenuItem[], targetKey: string): ParsedMenuItem | null => {
|
|
57
|
+
for (const item of items) {
|
|
58
|
+
if (item.key === targetKey) return item;
|
|
59
|
+
if (item.children) {
|
|
60
|
+
const found = findItem(item.children, targetKey);
|
|
61
|
+
if (found) return found;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
},
|
|
66
|
+
[],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// 处理菜单项点击
|
|
70
|
+
const handleClickMenuItem = useCallback(
|
|
71
|
+
(key: string) => {
|
|
72
|
+
const item = findItem(menuItems, key);
|
|
73
|
+
if (item?.path && item.type !== 'link') {
|
|
74
|
+
history.push(item.path);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
[menuItems, findItem],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// 处理侧边栏折叠
|
|
81
|
+
const handleCollapsed = useCallback(
|
|
82
|
+
(isCollapsed: boolean, type: 'clickTrigger' | 'responsive') => {
|
|
83
|
+
// 点击触发时切换到 hover 模式
|
|
84
|
+
if (
|
|
85
|
+
type === 'clickTrigger' &&
|
|
86
|
+
triggerMode === ETriggerMode.CLICK_TRIGGER
|
|
87
|
+
) {
|
|
88
|
+
setTriggerMode(ETriggerMode.HOVER_TRIGGER);
|
|
89
|
+
setCollapsed(isCollapsed);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// hover 模式下点击切换回 click 模式
|
|
94
|
+
if (triggerMode === ETriggerMode.HOVER_TRIGGER) {
|
|
95
|
+
setTriggerMode(ETriggerMode.CLICK_TRIGGER);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
[triggerMode],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
selectedKeys,
|
|
103
|
+
openKeys,
|
|
104
|
+
collapsed,
|
|
105
|
+
triggerMode,
|
|
106
|
+
handleClickMenuItem,
|
|
107
|
+
handleCollapsed,
|
|
108
|
+
setOpenKeys,
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default useMenuState;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyTheme,
|
|
3
|
+
getStoredTheme,
|
|
4
|
+
THEME,
|
|
5
|
+
type ThemeMode,
|
|
6
|
+
} from '@/common/theme';
|
|
7
|
+
import { useCallback, useEffect, useSyncExternalStore } from 'react';
|
|
8
|
+
|
|
9
|
+
// 主题变化订阅者列表
|
|
10
|
+
const subscribers = new Set<() => void>();
|
|
11
|
+
|
|
12
|
+
// 当前主题值(模块级别的共享状态)
|
|
13
|
+
let currentTheme: ThemeMode | null = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 通知所有订阅者主题已变化
|
|
17
|
+
*/
|
|
18
|
+
const notifySubscribers = (): void => {
|
|
19
|
+
subscribers.forEach((callback) => callback());
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 获取当前主题(用于 useSyncExternalStore)
|
|
24
|
+
*/
|
|
25
|
+
const getSnapshot = (): ThemeMode => {
|
|
26
|
+
if (currentTheme === null) {
|
|
27
|
+
currentTheme = getStoredTheme();
|
|
28
|
+
}
|
|
29
|
+
return currentTheme;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 订阅主题变化(用于 useSyncExternalStore)
|
|
34
|
+
*/
|
|
35
|
+
const subscribe = (callback: () => void): (() => void) => {
|
|
36
|
+
subscribers.add(callback);
|
|
37
|
+
return () => {
|
|
38
|
+
subscribers.delete(callback);
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 设置主题(内部方法)
|
|
44
|
+
*/
|
|
45
|
+
const setThemeInternal = (theme: ThemeMode, persist = true): void => {
|
|
46
|
+
if (currentTheme === theme) return;
|
|
47
|
+
|
|
48
|
+
currentTheme = theme;
|
|
49
|
+
applyTheme(theme);
|
|
50
|
+
|
|
51
|
+
if (persist) {
|
|
52
|
+
localStorage.setItem(THEME.STORAGE_KEY, theme);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 通知所有使用 useTheme 的组件
|
|
56
|
+
notifySubscribers();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 主题管理 Hook
|
|
61
|
+
*
|
|
62
|
+
* 使用 useSyncExternalStore 实现跨组件状态同步,
|
|
63
|
+
* 解决多个组件各自调用 useTheme 时状态不同步的问题。
|
|
64
|
+
*/
|
|
65
|
+
export const useTheme = () => {
|
|
66
|
+
// 使用 useSyncExternalStore 订阅共享的主题状态
|
|
67
|
+
const theme = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
68
|
+
|
|
69
|
+
// 首次挂载时应用主题
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
applyTheme(theme);
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
// 监听系统主题变化
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
77
|
+
|
|
78
|
+
const handleChange = (e: MediaQueryListEvent): void => {
|
|
79
|
+
// 只有在没有手动设置主题时才跟随系统
|
|
80
|
+
if (!localStorage.getItem(THEME.STORAGE_KEY)) {
|
|
81
|
+
const newTheme: ThemeMode = e.matches ? 'dark' : 'light';
|
|
82
|
+
setThemeInternal(newTheme, false);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
87
|
+
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
// 监听 storage 事件(其他标签页的主题变化)
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const handleStorage = (e: StorageEvent): void => {
|
|
93
|
+
if (e.key === THEME.STORAGE_KEY && e.newValue) {
|
|
94
|
+
const newTheme = e.newValue as ThemeMode;
|
|
95
|
+
if (newTheme === 'light' || newTheme === 'dark') {
|
|
96
|
+
setThemeInternal(newTheme, false);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
window.addEventListener('storage', handleStorage);
|
|
102
|
+
return () => window.removeEventListener('storage', handleStorage);
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
// 切换主题
|
|
106
|
+
const toggleTheme = useCallback((): void => {
|
|
107
|
+
const next: ThemeMode = currentTheme === 'light' ? 'dark' : 'light';
|
|
108
|
+
setThemeInternal(next, true);
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
// 设置指定主题
|
|
112
|
+
const setThemeValue = useCallback((newTheme: ThemeMode): void => {
|
|
113
|
+
setThemeInternal(newTheme, true);
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
theme,
|
|
118
|
+
toggleTheme,
|
|
119
|
+
setTheme: setThemeValue,
|
|
120
|
+
isDark: theme === 'dark',
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export default useTheme;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
@import '@/styles/variables.less';
|
|
2
|
+
|
|
3
|
+
// Common mixins
|
|
4
|
+
.header-gray-btn-mixin() {
|
|
5
|
+
background: @color-fill-2;
|
|
6
|
+
border-radius: @border-radius-button;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.center-mixin() {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.round-mixin(@size) {
|
|
16
|
+
width: @size;
|
|
17
|
+
height: @size;
|
|
18
|
+
border-radius: @border-radius-round;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.layout-header {
|
|
22
|
+
z-index: @header-z-index;
|
|
23
|
+
height: @header-height;
|
|
24
|
+
line-height: @header-height;
|
|
25
|
+
padding: 0 @spacing-lg;
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: space-between;
|
|
29
|
+
background: var(--color-bg-3);
|
|
30
|
+
box-shadow: 0 4px 4px 0 rgba(134, 144, 156, 8%);
|
|
31
|
+
flex-shrink: 0;
|
|
32
|
+
|
|
33
|
+
.trigger {
|
|
34
|
+
margin-left: 20px;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.layout-header-logo {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
margin-right: auto;
|
|
42
|
+
|
|
43
|
+
.logo-text {
|
|
44
|
+
font-size: @font-size-lg;
|
|
45
|
+
font-weight: @font-weight-bold;
|
|
46
|
+
color: @Brand1-6;
|
|
47
|
+
letter-spacing: 1px;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.layout-header-right {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.layout-header-action {
|
|
57
|
+
width: 32px;
|
|
58
|
+
height: 32px;
|
|
59
|
+
.center-mixin();
|
|
60
|
+
|
|
61
|
+
border-radius: @border-radius-round;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
transition: background-color @animation-duration-fast;
|
|
64
|
+
|
|
65
|
+
&:hover {
|
|
66
|
+
background: @color-fill-2;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
svg {
|
|
70
|
+
font-size: 18px;
|
|
71
|
+
color: @color-text-2;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.layout-header-user {
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
padding: @spacing-xs @spacing-sm;
|
|
79
|
+
border-radius: @border-radius-md;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
transition: background-color @animation-duration-fast;
|
|
82
|
+
|
|
83
|
+
&:hover {
|
|
84
|
+
background: @color-fill-2;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.user-name {
|
|
88
|
+
color: @color-text-1;
|
|
89
|
+
font-size: @font-size-base;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.split-line {
|
|
94
|
+
width: 1px;
|
|
95
|
+
height: 16px;
|
|
96
|
+
background: var(--color-border-2);
|
|
97
|
+
margin: 0 @spacing-xs;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.notification-btn {
|
|
101
|
+
.header-gray-btn-mixin();
|
|
102
|
+
.center-mixin();
|
|
103
|
+
|
|
104
|
+
color: @color-text-1;
|
|
105
|
+
|
|
106
|
+
.unread-count {
|
|
107
|
+
background-color: @Danger-6;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { getAuthInfo } from '@/common/auth/tool';
|
|
2
|
+
import { useTheme } from '@/hooks/useTheme';
|
|
3
|
+
import {
|
|
4
|
+
Avatar,
|
|
5
|
+
Divider,
|
|
6
|
+
Dropdown,
|
|
7
|
+
Layout,
|
|
8
|
+
Menu,
|
|
9
|
+
Space,
|
|
10
|
+
} from '@arco-design/web-react';
|
|
11
|
+
import {
|
|
12
|
+
IconMoonFill,
|
|
13
|
+
IconPoweroff,
|
|
14
|
+
IconSettings,
|
|
15
|
+
IconSunFill,
|
|
16
|
+
IconUser,
|
|
17
|
+
} from '@arco-design/web-react/icon';
|
|
18
|
+
import { useModel } from '@umijs/max';
|
|
19
|
+
import React, { useCallback } from 'react';
|
|
20
|
+
import './index.less';
|
|
21
|
+
|
|
22
|
+
const Header = Layout.Header;
|
|
23
|
+
|
|
24
|
+
const LayoutHeader: React.FC = () => {
|
|
25
|
+
const { initialState } = useModel('@@initialState');
|
|
26
|
+
const { theme, toggleTheme } = useTheme();
|
|
27
|
+
const authInfo = getAuthInfo();
|
|
28
|
+
|
|
29
|
+
const handleMenuClick = useCallback((key: string) => {
|
|
30
|
+
if (key === 'logout') {
|
|
31
|
+
// TODO: Implement logout logic
|
|
32
|
+
console.log('logout');
|
|
33
|
+
} else if (key === 'settings') {
|
|
34
|
+
// TODO: Navigate to settings page
|
|
35
|
+
console.log('settings');
|
|
36
|
+
}
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const droplist = (
|
|
40
|
+
<Menu onClickMenuItem={handleMenuClick}>
|
|
41
|
+
<Menu.Item key="settings">
|
|
42
|
+
<IconSettings style={{ marginRight: 8 }} />
|
|
43
|
+
设置
|
|
44
|
+
</Menu.Item>
|
|
45
|
+
<Divider style={{ margin: '4px 0' }} />
|
|
46
|
+
<Menu.Item key="logout">
|
|
47
|
+
<IconPoweroff style={{ marginRight: 8 }} />
|
|
48
|
+
退出登录
|
|
49
|
+
</Menu.Item>
|
|
50
|
+
</Menu>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Header className="layout-header">
|
|
55
|
+
{/* Logo */}
|
|
56
|
+
<div className="layout-header-logo">
|
|
57
|
+
<span className="logo-text">
|
|
58
|
+
{window.__MICO_MENUS__?.appName || 'AUDIT CENTER'}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* Right section */}
|
|
63
|
+
<div className="layout-header-right">
|
|
64
|
+
<Space size="medium">
|
|
65
|
+
{/* Theme toggle */}
|
|
66
|
+
<div
|
|
67
|
+
className="layout-header-action"
|
|
68
|
+
onClick={toggleTheme}
|
|
69
|
+
title={theme === 'dark' ? '切换到亮色模式' : '切换到暗色模式'}
|
|
70
|
+
>
|
|
71
|
+
{theme === 'dark' ? <IconSunFill /> : <IconMoonFill />}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<span className="split-line" />
|
|
75
|
+
|
|
76
|
+
{/* User info */}
|
|
77
|
+
<Dropdown droplist={droplist} position="br" trigger="click">
|
|
78
|
+
<div className="layout-header-user">
|
|
79
|
+
<Avatar size={32} style={{ marginRight: 8 }}>
|
|
80
|
+
{authInfo.avatar ? (
|
|
81
|
+
<img src={authInfo.avatar} alt="avatar" />
|
|
82
|
+
) : (
|
|
83
|
+
<IconUser />
|
|
84
|
+
)}
|
|
85
|
+
</Avatar>
|
|
86
|
+
<span className="user-name">
|
|
87
|
+
{authInfo.nickname || initialState?.currentUser?.name || '用户'}
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
</Dropdown>
|
|
91
|
+
</Space>
|
|
92
|
+
</div>
|
|
93
|
+
</Header>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default LayoutHeader;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
@import '@/styles/variables.less';
|
|
2
|
+
|
|
3
|
+
.arco-layout-sider-collapsed {
|
|
4
|
+
.layout-menu {
|
|
5
|
+
.layout-menu-level-1 {
|
|
6
|
+
// Handle safari text ellipsis in collapsed state
|
|
7
|
+
text-overflow: clip;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.layout-menu {
|
|
13
|
+
font-size: @font-size-base;
|
|
14
|
+
|
|
15
|
+
// Base menu item styles
|
|
16
|
+
.arco-menu-item {
|
|
17
|
+
height: 40px;
|
|
18
|
+
line-height: 40px;
|
|
19
|
+
padding: 0 12px;
|
|
20
|
+
|
|
21
|
+
&:hover {
|
|
22
|
+
background-color: @Brand1-1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.arco-menu-inline-header {
|
|
27
|
+
&:hover {
|
|
28
|
+
background-color: @Brand1-1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.arco-menu-inline-header.arco-menu-selected {
|
|
33
|
+
&:hover {
|
|
34
|
+
background-color: @Brand1-1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.arco-menu-item.arco-menu-selected {
|
|
39
|
+
background-color: @Brand1-1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.arco-menu-pop-header.arco-menu-selected {
|
|
43
|
+
background-color: @Brand1-1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.arco-menu-pop-header:hover {
|
|
47
|
+
background-color: @Brand1-1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.arco-menu-indent {
|
|
51
|
+
width: 32px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.layout-menu-level-3 {
|
|
55
|
+
margin-bottom: 0;
|
|
56
|
+
|
|
57
|
+
.arco-menu-indent {
|
|
58
|
+
width: 24px;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.arco-icon {
|
|
63
|
+
font-size: 16px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.arco-menu-inline-header .arco-icon {
|
|
67
|
+
margin-right: 16px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.arco-menu-icon-suffix {
|
|
71
|
+
.arco-icon {
|
|
72
|
+
font-size: 14px;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Level 1 menu spacing
|
|
77
|
+
&-level-1 {
|
|
78
|
+
margin-bottom: @spacing-sm;
|
|
79
|
+
|
|
80
|
+
&:last-child {
|
|
81
|
+
margin-bottom: 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Level 2 menu styles
|
|
86
|
+
.arco-menu-inner {
|
|
87
|
+
.arco-menu-sub-menu {
|
|
88
|
+
// Level 2 menu item spacing
|
|
89
|
+
.arco-menu-item {
|
|
90
|
+
margin-bottom: @spacing-xs;
|
|
91
|
+
|
|
92
|
+
&:last-child {
|
|
93
|
+
margin-bottom: 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Spacing between level 1 and level 2 menus
|
|
98
|
+
> .arco-menu-item {
|
|
99
|
+
margin-top: @spacing-xs;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Level 3 menu styles
|
|
104
|
+
.arco-menu-sub-menu .arco-menu-sub-menu {
|
|
105
|
+
// Level 3 menu item spacing is 0
|
|
106
|
+
.arco-menu-item {
|
|
107
|
+
margin-bottom: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Level 3 menu indent relative to level 2
|
|
111
|
+
.arco-menu-inner {
|
|
112
|
+
padding-left: 56px;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Submenu expanded styles
|
|
118
|
+
.arco-menu-sub-menu {
|
|
119
|
+
.arco-menu-sub-menu-title {
|
|
120
|
+
height: 40px;
|
|
121
|
+
line-height: 40px;
|
|
122
|
+
padding: 0 16px;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Limit to max 3 levels
|
|
127
|
+
.arco-menu-sub-menu .arco-menu-sub-menu .arco-menu-sub-menu {
|
|
128
|
+
// Hide level 4+ menus
|
|
129
|
+
.arco-menu-sub-menu {
|
|
130
|
+
display: none;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Sider styles
|
|
136
|
+
.<%= projectName %>-sider {
|
|
137
|
+
box-shadow: none;
|
|
138
|
+
|
|
139
|
+
.arco-layout-sider-trigger {
|
|
140
|
+
display: flex;
|
|
141
|
+
justify-content: flex-end;
|
|
142
|
+
// 使用主题变量适配亮/暗主题
|
|
143
|
+
background-color: @color-text-5;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.click-trigger-btn {
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
display: flex;
|
|
149
|
+
|
|
150
|
+
// 图标颜色适配主题
|
|
151
|
+
.arco-icon {
|
|
152
|
+
color: @color-text-2;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.click-trigger-btn:not(.collapsed) {
|
|
157
|
+
background-color: @color-fill-1;
|
|
158
|
+
border-radius: @border-radius-lg;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.click-trigger-btn.collapsed {
|
|
162
|
+
justify-content: center;
|
|
163
|
+
}
|
|
164
|
+
}
|