ess-main-template 1.0.0

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 (68) hide show
  1. package/.env.example +7 -0
  2. package/.prettierrc +8 -0
  3. package/README.md +73 -0
  4. package/commitlint.config.js +6 -0
  5. package/eslint.config.js +32 -0
  6. package/index.html +22 -0
  7. package/mock/auth.mock.ts +39 -0
  8. package/mock/user.mock.ts +83 -0
  9. package/package.json +64 -0
  10. package/public/favicon.svg +1 -0
  11. package/public/icons.svg +24 -0
  12. package/src/App.css +182 -0
  13. package/src/App.tsx +29 -0
  14. package/src/assets/fonts/.gitkeep +0 -0
  15. package/src/assets/hero.png +0 -0
  16. package/src/assets/react.svg +1 -0
  17. package/src/assets/vite.svg +1 -0
  18. package/src/components/AuthButton.tsx +23 -0
  19. package/src/components/LangSwitch.tsx +31 -0
  20. package/src/components/RouteProgress.tsx +25 -0
  21. package/src/components/StationPicker.tsx +24 -0
  22. package/src/components/ThemeSwitch.tsx +18 -0
  23. package/src/constants/bus-events.ts +15 -0
  24. package/src/constants/index.ts +25 -0
  25. package/src/hooks/useAuth.ts +17 -0
  26. package/src/i18n/index.ts +32 -0
  27. package/src/i18n/locales/en/auth.json +15 -0
  28. package/src/i18n/locales/en/common.json +33 -0
  29. package/src/i18n/locales/en/menu.json +33 -0
  30. package/src/i18n/locales/zh/auth.json +15 -0
  31. package/src/i18n/locales/zh/common.json +33 -0
  32. package/src/i18n/locales/zh/menu.json +33 -0
  33. package/src/index.css +109 -0
  34. package/src/layouts/BasicLayout.tsx +73 -0
  35. package/src/layouts/BlankLayout.tsx +7 -0
  36. package/src/layouts/components/RightContent.tsx +43 -0
  37. package/src/main.tsx +13 -0
  38. package/src/pages/403.tsx +23 -0
  39. package/src/pages/404.tsx +23 -0
  40. package/src/pages/Dashboard/index.tsx +15 -0
  41. package/src/pages/Login/index.module.less +23 -0
  42. package/src/pages/Login/index.tsx +58 -0
  43. package/src/router/AppEntry.tsx +93 -0
  44. package/src/router/AuthGuard.tsx +19 -0
  45. package/src/router/componentMap.ts +14 -0
  46. package/src/router/generateRoutes.tsx +55 -0
  47. package/src/router/index.tsx +18 -0
  48. package/src/services/auth.ts +42 -0
  49. package/src/store/appStore.ts +51 -0
  50. package/src/store/userStore.ts +81 -0
  51. package/src/styles/global.less +40 -0
  52. package/src/styles/mixins.less +56 -0
  53. package/src/styles/reset.less +49 -0
  54. package/src/styles/variables.less +50 -0
  55. package/src/types/i18next.d.ts +16 -0
  56. package/src/types/less.d.ts +6 -0
  57. package/src/types/wujie-react.d.ts +37 -0
  58. package/src/utils/auth.ts +28 -0
  59. package/src/utils/request.ts +118 -0
  60. package/src/wujie/SubApp.tsx +72 -0
  61. package/src/wujie/bus.ts +38 -0
  62. package/src/wujie/config.ts +33 -0
  63. package/src/wujie/props.ts +42 -0
  64. package/src/wujie/useBusSync.ts +67 -0
  65. package/tsconfig.app.json +34 -0
  66. package/tsconfig.json +4 -0
  67. package/tsconfig.node.json +24 -0
  68. package/vite.config.ts +26 -0
@@ -0,0 +1,23 @@
1
+ import { useAuth } from '@/hooks/useAuth'
2
+
3
+ interface AuthButtonProps {
4
+ /** 所需权限码,拥有任一即可显示 */
5
+ code: string | string[]
6
+ /** 无权限时的替代渲染(默认不渲染) */
7
+ fallback?: React.ReactNode
8
+ children: React.ReactNode
9
+ }
10
+
11
+ /** 权限按钮包裹组件:根据权限码决定子元素是否可见 */
12
+ const AuthButton: React.FC<AuthButtonProps> = ({ code, fallback = null, children }) => {
13
+ const { hasPermission, hasAnyPermission } = useAuth()
14
+
15
+ const codes = Array.isArray(code) ? code : [code]
16
+ const authorized = codes.length === 1 ? hasPermission(codes[0]) : hasAnyPermission(codes)
17
+
18
+ if (!authorized) return <>{fallback}</>
19
+
20
+ return <>{children}</>
21
+ }
22
+
23
+ export default AuthButton
@@ -0,0 +1,31 @@
1
+ import { Select } from 'antd'
2
+ import { GlobalOutlined } from '@ant-design/icons'
3
+ import { useTranslation } from 'react-i18next'
4
+ import { useAppStore } from '@/store/appStore'
5
+ import type { Locale } from '@/store/appStore'
6
+
7
+ const LangSwitch: React.FC = () => {
8
+ const { i18n } = useTranslation()
9
+ const { locale, setLocale } = useAppStore()
10
+
11
+ const handleChange = (value: Locale) => {
12
+ setLocale(value)
13
+ i18n.changeLanguage(value)
14
+ }
15
+
16
+ return (
17
+ <Select
18
+ value={locale}
19
+ onChange={handleChange}
20
+ variant="borderless"
21
+ suffixIcon={<GlobalOutlined />}
22
+ options={[
23
+ { value: 'zh', label: '中文' },
24
+ { value: 'en', label: 'English' },
25
+ ]}
26
+ style={{ width: 100 }}
27
+ />
28
+ )
29
+ }
30
+
31
+ export default LangSwitch
@@ -0,0 +1,25 @@
1
+ import { useEffect } from 'react'
2
+ import { useLocation } from 'react-router-dom'
3
+ import NProgress from 'nprogress'
4
+ import 'nprogress/nprogress.css'
5
+
6
+ NProgress.configure({ showSpinner: false, speed: 300 })
7
+
8
+ /** 路由切换时显示顶部进度条 */
9
+ const RouteProgress: React.FC = () => {
10
+ const location = useLocation()
11
+
12
+ useEffect(() => {
13
+ NProgress.start()
14
+ // Suspense 懒加载完成后,组件已渲染,此时 done
15
+ const timer = setTimeout(() => NProgress.done(), 100)
16
+ return () => {
17
+ clearTimeout(timer)
18
+ NProgress.done()
19
+ }
20
+ }, [location.pathname])
21
+
22
+ return null
23
+ }
24
+
25
+ export default RouteProgress
@@ -0,0 +1,24 @@
1
+ import { Select } from 'antd'
2
+ import { EnvironmentOutlined } from '@ant-design/icons'
3
+ import { useAppStore } from '@/store/appStore'
4
+ import { useUserStore } from '@/store/userStore'
5
+
6
+ const StationPicker: React.FC = () => {
7
+ const { currentStation, setCurrentStation } = useAppStore()
8
+ const authorizedStations = useUserStore((s) => s.authorizedStations)
9
+
10
+ if (!authorizedStations.length) return null
11
+
12
+ return (
13
+ <Select
14
+ value={currentStation || authorizedStations[0]?.id}
15
+ onChange={setCurrentStation}
16
+ variant="borderless"
17
+ suffixIcon={<EnvironmentOutlined />}
18
+ options={authorizedStations.map((s) => ({ value: s.id, label: s.name }))}
19
+ style={{ width: 140 }}
20
+ />
21
+ )
22
+ }
23
+
24
+ export default StationPicker
@@ -0,0 +1,18 @@
1
+ import { Switch } from 'antd'
2
+ import { BulbOutlined } from '@ant-design/icons'
3
+ import { useAppStore } from '@/store/appStore'
4
+
5
+ const ThemeSwitch: React.FC = () => {
6
+ const { theme, toggleTheme } = useAppStore()
7
+
8
+ return (
9
+ <Switch
10
+ checkedChildren={<BulbOutlined />}
11
+ unCheckedChildren={<BulbOutlined />}
12
+ checked={theme === 'dark'}
13
+ onChange={toggleTheme}
14
+ />
15
+ )
16
+ }
17
+
18
+ export default ThemeSwitch
@@ -0,0 +1,15 @@
1
+ /** wujie bus 事件名常量 */
2
+ export const BUS_EVENTS = {
3
+ /** 语言切换 */
4
+ LOCALE_CHANGE: 'locale-change',
5
+ /** 主题切换 */
6
+ THEME_CHANGE: 'theme-change',
7
+ /** 站点切换 */
8
+ STATION_CHANGE: 'station-change',
9
+ /** Token 已刷新 */
10
+ TOKEN_REFRESH: 'token-refresh',
11
+ /** Token 过期(子应用通知主应用) */
12
+ TOKEN_EXPIRED: 'token-expired',
13
+ /** 路由跳转(子应用通知主应用) */
14
+ NAVIGATE: 'navigate',
15
+ } as const
@@ -0,0 +1,25 @@
1
+ /** localStorage 存储键 */
2
+ export const STORAGE_KEYS = {
3
+ ACCESS_TOKEN: 'ess_access_token',
4
+ REFRESH_TOKEN: 'ess_refresh_token',
5
+ USER_INFO: 'ess_user_info',
6
+ LOCALE: 'ess_locale',
7
+ THEME: 'ess_theme',
8
+ } as const
9
+
10
+ /** API 基础地址 */
11
+ export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string
12
+
13
+ /** 子应用地址 */
14
+ export const SUB_APP_URLS = {
15
+ OPERATION: import.meta.env.VITE_SUB_OPERATION_URL as string,
16
+ ANALYSIS: import.meta.env.VITE_SUB_ANALYSIS_URL as string,
17
+ } as const
18
+
19
+ /** Token 刷新相关 */
20
+ export const TOKEN_CONFIG = {
21
+ /** 最大重试次数 */
22
+ MAX_RETRY: 2,
23
+ /** 重试间隔基数(ms) */
24
+ RETRY_DELAY: 1000,
25
+ } as const
@@ -0,0 +1,17 @@
1
+ import { useUserStore } from '@/store/userStore'
2
+
3
+ /** 检查当前用户是否拥有指定权限 */
4
+ export function useAuth() {
5
+ const permissions = useUserStore((s) => s.permissions)
6
+
7
+ /** 是否拥有某个权限 */
8
+ const hasPermission = (code: string) => permissions.includes(code)
9
+
10
+ /** 是否拥有所有指定权限 */
11
+ const hasAllPermissions = (codes: string[]) => codes.every((c) => permissions.includes(c))
12
+
13
+ /** 是否拥有任一指定权限 */
14
+ const hasAnyPermission = (codes: string[]) => codes.some((c) => permissions.includes(c))
15
+
16
+ return { permissions, hasPermission, hasAllPermissions, hasAnyPermission }
17
+ }
@@ -0,0 +1,32 @@
1
+ import i18n from 'i18next'
2
+ import { initReactI18next } from 'react-i18next'
3
+ import HttpBackend from 'i18next-http-backend'
4
+ import { STORAGE_KEYS } from '@/constants'
5
+
6
+ // 获取保存的语言偏好,默认中文
7
+ const savedLocale = localStorage.getItem(STORAGE_KEYS.LOCALE) || 'zh'
8
+
9
+ i18n
10
+ .use(HttpBackend)
11
+ .use(initReactI18next)
12
+ .init({
13
+ lng: savedLocale,
14
+ fallbackLng: 'zh',
15
+ ns: ['common', 'menu', 'auth'],
16
+ defaultNS: 'common',
17
+
18
+ backend: {
19
+ // 按语言和命名空间加载 JSON 文件
20
+ loadPath: '/src/i18n/locales/{{lng}}/{{ns}}.json',
21
+ },
22
+
23
+ interpolation: {
24
+ escapeValue: false, // React 已默认转义
25
+ },
26
+
27
+ react: {
28
+ useSuspense: true,
29
+ },
30
+ })
31
+
32
+ export default i18n
@@ -0,0 +1,15 @@
1
+ {
2
+ "login": "Login",
3
+ "logout": "Logout",
4
+ "loginTitle": "Energy Storage O&M Platform",
5
+ "username": "Username",
6
+ "password": "Password",
7
+ "usernamePlaceholder": "Enter username",
8
+ "passwordPlaceholder": "Enter password",
9
+ "loginButton": "Sign In",
10
+ "loginSuccess": "Login successful",
11
+ "loginFailed": "Login failed",
12
+ "usernameRequired": "Please enter username",
13
+ "passwordRequired": "Please enter password",
14
+ "tokenExpired": "Session expired, please login again"
15
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "appName": "ESS Operation & Maintenance Platform",
3
+ "loading": "Loading...",
4
+ "confirm": "Confirm",
5
+ "cancel": "Cancel",
6
+ "save": "Save",
7
+ "delete": "Delete",
8
+ "edit": "Edit",
9
+ "add": "Add",
10
+ "search": "Search",
11
+ "reset": "Reset",
12
+ "export": "Export",
13
+ "import": "Import",
14
+ "operation": "Operation",
15
+ "status": "Status",
16
+ "enable": "Enable",
17
+ "disable": "Disable",
18
+ "success": "Operation successful",
19
+ "failed": "Operation failed",
20
+ "noData": "No data",
21
+ "networkError": "Network disconnected, please check your connection",
22
+ "serverError": "Server error, please try again later",
23
+ "noPermission": "You do not have permission to perform this action",
24
+ "systemName": "Energy Storage O&M Platform",
25
+ "logout": "Logout",
26
+ "backHome": "Back to Home",
27
+ "retry": "Retry",
28
+ "subAppLoading": "Loading sub-app...",
29
+ "subAppTimeout": "Sub-app {{name}} load timeout",
30
+ "subAppLoadError": "Sub-app {{name}} load failed",
31
+ "subAppNotFound": "Sub-app not found",
32
+ "subAppError": "Sub-app load error"
33
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "dashboard": "Dashboard",
3
+ "home": "Home",
4
+ "operation": "Operation",
5
+ "system": "System",
6
+ "systemUser": "Users",
7
+ "systemRole": "Roles",
8
+ "systemPermission": "Permissions",
9
+ "systemDict": "Dictionary",
10
+ "systemLog": "Audit Log",
11
+ "device": "Devices",
12
+ "deviceList": "Device List",
13
+ "deviceCategory": "Device Category",
14
+ "deviceDetail": "Device Detail",
15
+ "alarm": "Alarms",
16
+ "alarmRealtime": "Real-time Alarms",
17
+ "alarmHistory": "Alarm History",
18
+ "alarmRules": "Alarm Rules",
19
+ "workOrder": "Work Orders",
20
+ "workOrderList": "Order List",
21
+ "workOrderCreate": "Create Order",
22
+ "workOrderDetail": "Order Detail",
23
+ "inspection": "Inspection",
24
+ "analysis": "Analytics",
25
+ "energyStats": "Energy Statistics",
26
+ "efficiency": "Efficiency",
27
+ "revenue": "Revenue",
28
+ "batteryHealth": "Battery Health",
29
+ "report": "Reports",
30
+ "reportDaily": "Daily Report",
31
+ "reportMonthly": "Monthly Report",
32
+ "reportCustom": "Custom Report"
33
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "login": "登录",
3
+ "logout": "退出登录",
4
+ "loginTitle": "储能运维管理平台",
5
+ "username": "用户名",
6
+ "password": "密码",
7
+ "usernamePlaceholder": "请输入用户名",
8
+ "passwordPlaceholder": "请输入密码",
9
+ "loginButton": "登 录",
10
+ "loginSuccess": "登录成功",
11
+ "loginFailed": "登录失败",
12
+ "usernameRequired": "请输入用户名",
13
+ "passwordRequired": "请输入密码",
14
+ "tokenExpired": "登录已过期,请重新登录"
15
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "appName": "储能运维管理平台",
3
+ "loading": "加载中...",
4
+ "confirm": "确认",
5
+ "cancel": "取消",
6
+ "save": "保存",
7
+ "delete": "删除",
8
+ "edit": "编辑",
9
+ "add": "新增",
10
+ "search": "搜索",
11
+ "reset": "重置",
12
+ "export": "导出",
13
+ "import": "导入",
14
+ "operation": "操作",
15
+ "status": "状态",
16
+ "enable": "启用",
17
+ "disable": "禁用",
18
+ "success": "操作成功",
19
+ "failed": "操作失败",
20
+ "noData": "暂无数据",
21
+ "networkError": "网络已断开,请检查网络连接",
22
+ "serverError": "服务异常,请稍后重试",
23
+ "noPermission": "没有权限执行此操作",
24
+ "systemName": "储能运维管理平台",
25
+ "logout": "退出登录",
26
+ "backHome": "返回首页",
27
+ "retry": "重试",
28
+ "subAppLoading": "子应用加载中...",
29
+ "subAppTimeout": "子应用 {{name}} 加载超时",
30
+ "subAppLoadError": "子应用 {{name}} 加载失败",
31
+ "subAppNotFound": "子应用未找到",
32
+ "subAppError": "子应用加载异常"
33
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "dashboard": "仪表盘",
3
+ "home": "首页",
4
+ "operation": "运维管理",
5
+ "system": "系统管理",
6
+ "systemUser": "用户管理",
7
+ "systemRole": "角色管理",
8
+ "systemPermission": "权限管理",
9
+ "systemDict": "字典管理",
10
+ "systemLog": "操作日志",
11
+ "device": "设备管理",
12
+ "deviceList": "设备台账",
13
+ "deviceCategory": "设备分类",
14
+ "deviceDetail": "设备详情",
15
+ "alarm": "告警管理",
16
+ "alarmRealtime": "实时告警",
17
+ "alarmHistory": "历史告警",
18
+ "alarmRules": "告警规则",
19
+ "workOrder": "运维工单",
20
+ "workOrderList": "工单列表",
21
+ "workOrderCreate": "创建工单",
22
+ "workOrderDetail": "工单详情",
23
+ "inspection": "巡检计划",
24
+ "analysis": "数据分析",
25
+ "energyStats": "充放电统计",
26
+ "efficiency": "能量效率",
27
+ "revenue": "收益分析",
28
+ "batteryHealth": "电池健康度",
29
+ "report": "报表中心",
30
+ "reportDaily": "日报",
31
+ "reportMonthly": "月报",
32
+ "reportCustom": "自定义报表"
33
+ }
package/src/index.css ADDED
@@ -0,0 +1,109 @@
1
+ :root {
2
+ --text: #6b6375;
3
+ --text-h: #08060d;
4
+ --bg: #fff;
5
+ --border: #e5e4e7;
6
+ --code-bg: #f4f3ec;
7
+ --accent: #aa3bff;
8
+ --accent-bg: rgba(170, 59, 255, 0.1);
9
+ --accent-border: rgba(170, 59, 255, 0.5);
10
+ --social-bg: rgba(244, 243, 236, 0.5);
11
+ --shadow: rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
12
+
13
+ --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
14
+ --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
15
+ --mono: ui-monospace, Consolas, monospace;
16
+
17
+ font: 18px/145% var(--sans);
18
+ letter-spacing: 0.18px;
19
+ color-scheme: light dark;
20
+ color: var(--text);
21
+ background: var(--bg);
22
+ font-synthesis: none;
23
+ text-rendering: optimizeLegibility;
24
+ -webkit-font-smoothing: antialiased;
25
+ -moz-osx-font-smoothing: grayscale;
26
+
27
+ @media (max-width: 1024px) {
28
+ font-size: 16px;
29
+ }
30
+ }
31
+
32
+ @media (prefers-color-scheme: dark) {
33
+ :root {
34
+ --text: #9ca3af;
35
+ --text-h: #f3f4f6;
36
+ --bg: #16171d;
37
+ --border: #2e303a;
38
+ --code-bg: #1f2028;
39
+ --accent: #c084fc;
40
+ --accent-bg: rgba(192, 132, 252, 0.15);
41
+ --accent-border: rgba(192, 132, 252, 0.5);
42
+ --social-bg: rgba(47, 48, 58, 0.5);
43
+ --shadow: rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
44
+ }
45
+
46
+ #social .button-icon {
47
+ filter: invert(1) brightness(2);
48
+ }
49
+ }
50
+
51
+ #root {
52
+ width: 1126px;
53
+ max-width: 100%;
54
+ margin: 0 auto;
55
+ text-align: center;
56
+ border-inline: 1px solid var(--border);
57
+ min-height: 100svh;
58
+ display: flex;
59
+ flex-direction: column;
60
+ box-sizing: border-box;
61
+ }
62
+
63
+ body {
64
+ margin: 0;
65
+ }
66
+
67
+ h1,
68
+ h2 {
69
+ font-family: var(--heading);
70
+ font-weight: 500;
71
+ color: var(--text-h);
72
+ }
73
+
74
+ h1 {
75
+ font-size: 56px;
76
+ letter-spacing: -1.68px;
77
+ margin: 32px 0;
78
+ @media (max-width: 1024px) {
79
+ font-size: 36px;
80
+ margin: 20px 0;
81
+ }
82
+ }
83
+ h2 {
84
+ font-size: 24px;
85
+ line-height: 118%;
86
+ letter-spacing: -0.24px;
87
+ margin: 0 0 8px;
88
+ @media (max-width: 1024px) {
89
+ font-size: 20px;
90
+ }
91
+ }
92
+ p {
93
+ margin: 0;
94
+ }
95
+
96
+ code,
97
+ .counter {
98
+ font-family: var(--mono);
99
+ display: inline-flex;
100
+ border-radius: 4px;
101
+ color: var(--text-h);
102
+ }
103
+
104
+ code {
105
+ font-size: 15px;
106
+ line-height: 135%;
107
+ padding: 4px 8px;
108
+ background: var(--code-bg);
109
+ }
@@ -0,0 +1,73 @@
1
+ import { useState, useMemo } from 'react'
2
+ import { Outlet, useNavigate, useLocation } from 'react-router-dom'
3
+ import { ProLayout } from '@ant-design/pro-components'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { useAppStore } from '@/store/appStore'
6
+ import { useUserStore } from '@/store/userStore'
7
+ import RouteProgress from '@/components/RouteProgress'
8
+ import { useBusSync } from '@/wujie/useBusSync'
9
+ import type { RouteItem } from '@/store/userStore'
10
+ import RightContent from './components/RightContent'
11
+
12
+ /** 将后端路由数据转为 ProLayout route 格式,使用 i18n 翻译菜单名 */
13
+ function toProLayoutRoutes(
14
+ routes: RouteItem[],
15
+ t: (key: string) => string,
16
+ ): { path: string; name: string; children?: ReturnType<typeof toProLayoutRoutes> }[] {
17
+ return routes.map((r) => ({
18
+ path: r.path,
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ name: (t as any)(r.name),
21
+ ...(r.children?.length ? { children: toProLayoutRoutes(r.children, t) } : {}),
22
+ }))
23
+ }
24
+
25
+ const BasicLayout: React.FC = () => {
26
+ const navigate = useNavigate()
27
+ const location = useLocation()
28
+ const { t } = useTranslation('common')
29
+ const { t: tMenu } = useTranslation('menu')
30
+ const { sidebarCollapsed, setSidebarCollapsed } = useAppStore()
31
+ const dynamicRoutes = useUserStore((s) => s.dynamicRoutes)
32
+ const [pathname, setPathname] = useState(location.pathname)
33
+
34
+ // 主应用与子应用的 bus 事件同步
35
+ useBusSync()
36
+
37
+ const menuRoutes = useMemo(
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ () => toProLayoutRoutes(dynamicRoutes, tMenu as any),
40
+ [dynamicRoutes, tMenu],
41
+ )
42
+
43
+ return (
44
+ <ProLayout
45
+ title={t('systemName')}
46
+ logo={null}
47
+ layout="mix"
48
+ collapsed={sidebarCollapsed}
49
+ onCollapse={setSidebarCollapsed}
50
+ location={{ pathname }}
51
+ route={{
52
+ path: '/',
53
+ children: menuRoutes,
54
+ }}
55
+ menuItemRender={(item, dom) => (
56
+ <a
57
+ onClick={() => {
58
+ setPathname(item.path || '/')
59
+ navigate(item.path || '/')
60
+ }}
61
+ >
62
+ {dom}
63
+ </a>
64
+ )}
65
+ actionsRender={() => [<RightContent key="right" />]}
66
+ >
67
+ <RouteProgress />
68
+ <Outlet />
69
+ </ProLayout>
70
+ )
71
+ }
72
+
73
+ export default BasicLayout
@@ -0,0 +1,7 @@
1
+ import { Outlet } from 'react-router-dom'
2
+
3
+ const BlankLayout: React.FC = () => {
4
+ return <Outlet />
5
+ }
6
+
7
+ export default BlankLayout
@@ -0,0 +1,43 @@
1
+ import { Space, Dropdown } from 'antd'
2
+ import { UserOutlined } from '@ant-design/icons'
3
+ import { useTranslation } from 'react-i18next'
4
+ import { useNavigate } from 'react-router-dom'
5
+ import { useUserStore } from '@/store/userStore'
6
+ import ThemeSwitch from '@/components/ThemeSwitch'
7
+ import LangSwitch from '@/components/LangSwitch'
8
+ import StationPicker from '@/components/StationPicker'
9
+
10
+ const RightContent: React.FC = () => {
11
+ const { t } = useTranslation('common')
12
+ const navigate = useNavigate()
13
+ const { userInfo, logout } = useUserStore()
14
+
15
+ const handleLogout = () => {
16
+ logout()
17
+ navigate('/login', { replace: true })
18
+ }
19
+
20
+ return (
21
+ <Space size="middle">
22
+ <StationPicker />
23
+ <ThemeSwitch />
24
+ <LangSwitch />
25
+
26
+ <Dropdown
27
+ menu={{
28
+ items: [{ key: 'logout', label: t('logout'), danger: true }],
29
+ onClick: ({ key }) => {
30
+ if (key === 'logout') handleLogout()
31
+ },
32
+ }}
33
+ >
34
+ <Space style={{ cursor: 'pointer' }}>
35
+ <UserOutlined />
36
+ <span>{userInfo?.realName || userInfo?.username || '-'}</span>
37
+ </Space>
38
+ </Dropdown>
39
+ </Space>
40
+ )
41
+ }
42
+
43
+ export default RightContent
package/src/main.tsx ADDED
@@ -0,0 +1,13 @@
1
+ import { StrictMode, Suspense } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './i18n'
4
+ import './styles/global.less'
5
+ import App from './App.tsx'
6
+
7
+ createRoot(document.getElementById('root')!).render(
8
+ <StrictMode>
9
+ <Suspense fallback={<div>Loading...</div>}>
10
+ <App />
11
+ </Suspense>
12
+ </StrictMode>,
13
+ )
@@ -0,0 +1,23 @@
1
+ import { Button, Result } from 'antd'
2
+ import { useNavigate } from 'react-router-dom'
3
+ import { useTranslation } from 'react-i18next'
4
+
5
+ const Forbidden: React.FC = () => {
6
+ const navigate = useNavigate()
7
+ const { t } = useTranslation('common')
8
+
9
+ return (
10
+ <Result
11
+ status="403"
12
+ title="403"
13
+ subTitle={t('noPermission')}
14
+ extra={
15
+ <Button type="primary" onClick={() => navigate('/')}>
16
+ {t('backHome')}
17
+ </Button>
18
+ }
19
+ />
20
+ )
21
+ }
22
+
23
+ export default Forbidden