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.
- package/.env.example +7 -0
- package/.prettierrc +8 -0
- package/README.md +73 -0
- package/commitlint.config.js +6 -0
- package/eslint.config.js +32 -0
- package/index.html +22 -0
- package/mock/auth.mock.ts +39 -0
- package/mock/user.mock.ts +83 -0
- package/package.json +64 -0
- package/public/favicon.svg +1 -0
- package/public/icons.svg +24 -0
- package/src/App.css +182 -0
- package/src/App.tsx +29 -0
- package/src/assets/fonts/.gitkeep +0 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/components/AuthButton.tsx +23 -0
- package/src/components/LangSwitch.tsx +31 -0
- package/src/components/RouteProgress.tsx +25 -0
- package/src/components/StationPicker.tsx +24 -0
- package/src/components/ThemeSwitch.tsx +18 -0
- package/src/constants/bus-events.ts +15 -0
- package/src/constants/index.ts +25 -0
- package/src/hooks/useAuth.ts +17 -0
- package/src/i18n/index.ts +32 -0
- package/src/i18n/locales/en/auth.json +15 -0
- package/src/i18n/locales/en/common.json +33 -0
- package/src/i18n/locales/en/menu.json +33 -0
- package/src/i18n/locales/zh/auth.json +15 -0
- package/src/i18n/locales/zh/common.json +33 -0
- package/src/i18n/locales/zh/menu.json +33 -0
- package/src/index.css +109 -0
- package/src/layouts/BasicLayout.tsx +73 -0
- package/src/layouts/BlankLayout.tsx +7 -0
- package/src/layouts/components/RightContent.tsx +43 -0
- package/src/main.tsx +13 -0
- package/src/pages/403.tsx +23 -0
- package/src/pages/404.tsx +23 -0
- package/src/pages/Dashboard/index.tsx +15 -0
- package/src/pages/Login/index.module.less +23 -0
- package/src/pages/Login/index.tsx +58 -0
- package/src/router/AppEntry.tsx +93 -0
- package/src/router/AuthGuard.tsx +19 -0
- package/src/router/componentMap.ts +14 -0
- package/src/router/generateRoutes.tsx +55 -0
- package/src/router/index.tsx +18 -0
- package/src/services/auth.ts +42 -0
- package/src/store/appStore.ts +51 -0
- package/src/store/userStore.ts +81 -0
- package/src/styles/global.less +40 -0
- package/src/styles/mixins.less +56 -0
- package/src/styles/reset.less +49 -0
- package/src/styles/variables.less +50 -0
- package/src/types/i18next.d.ts +16 -0
- package/src/types/less.d.ts +6 -0
- package/src/types/wujie-react.d.ts +37 -0
- package/src/utils/auth.ts +28 -0
- package/src/utils/request.ts +118 -0
- package/src/wujie/SubApp.tsx +72 -0
- package/src/wujie/bus.ts +38 -0
- package/src/wujie/config.ts +33 -0
- package/src/wujie/props.ts +42 -0
- package/src/wujie/useBusSync.ts +67 -0
- package/tsconfig.app.json +34 -0
- package/tsconfig.json +4 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +26 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// 品牌色(储能行业 - 绿色系)
|
|
2
|
+
@color-primary: #00b96b;
|
|
3
|
+
@color-info: #1890ff;
|
|
4
|
+
@color-error: #ff4d4f;
|
|
5
|
+
@color-warning: #faad14;
|
|
6
|
+
@color-success: #52c41a;
|
|
7
|
+
|
|
8
|
+
// 文字色
|
|
9
|
+
@text-color: rgba(0, 0, 0, 0.88);
|
|
10
|
+
@text-color-secondary: rgba(0, 0, 0, 0.65);
|
|
11
|
+
@text-color-disabled: rgba(0, 0, 0, 0.25);
|
|
12
|
+
|
|
13
|
+
// 背景色
|
|
14
|
+
@bg-layout: #f0f2f5;
|
|
15
|
+
@bg-container: #ffffff;
|
|
16
|
+
|
|
17
|
+
// 间距
|
|
18
|
+
@spacing-xs: 4px;
|
|
19
|
+
@spacing-sm: 8px;
|
|
20
|
+
@spacing-md: 16px;
|
|
21
|
+
@spacing-lg: 24px;
|
|
22
|
+
@spacing-xl: 32px;
|
|
23
|
+
|
|
24
|
+
// 圆角
|
|
25
|
+
@border-radius-sm: 4px;
|
|
26
|
+
@border-radius-base: 6px;
|
|
27
|
+
@border-radius-lg: 8px;
|
|
28
|
+
|
|
29
|
+
// 字体
|
|
30
|
+
@font-family:
|
|
31
|
+
'CustomFont',
|
|
32
|
+
-apple-system,
|
|
33
|
+
BlinkMacSystemFont,
|
|
34
|
+
'Segoe UI',
|
|
35
|
+
Roboto,
|
|
36
|
+
sans-serif;
|
|
37
|
+
@font-size-sm: 12px;
|
|
38
|
+
@font-size-base: 14px;
|
|
39
|
+
@font-size-lg: 16px;
|
|
40
|
+
@font-size-xl: 20px;
|
|
41
|
+
|
|
42
|
+
// 阴影
|
|
43
|
+
@box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
44
|
+
@box-shadow-card:
|
|
45
|
+
0 1px 2px rgba(0, 0, 0, 0.03),
|
|
46
|
+
0 1px 6px -1px rgba(0, 0, 0, 0.02),
|
|
47
|
+
0 2px 4px rgba(0, 0, 0, 0.02);
|
|
48
|
+
|
|
49
|
+
// 过渡
|
|
50
|
+
@transition-duration: 0.3s;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import 'i18next'
|
|
2
|
+
|
|
3
|
+
import type zhCommon from '../i18n/locales/zh/common.json'
|
|
4
|
+
import type zhMenu from '../i18n/locales/zh/menu.json'
|
|
5
|
+
import type zhAuth from '../i18n/locales/zh/auth.json'
|
|
6
|
+
|
|
7
|
+
declare module 'i18next' {
|
|
8
|
+
interface CustomTypeOptions {
|
|
9
|
+
defaultNS: 'common'
|
|
10
|
+
resources: {
|
|
11
|
+
common: typeof zhCommon
|
|
12
|
+
menu: typeof zhMenu
|
|
13
|
+
auth: typeof zhAuth
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
declare module 'wujie-react' {
|
|
2
|
+
import type { Component } from 'react'
|
|
3
|
+
|
|
4
|
+
interface WujieReactProps {
|
|
5
|
+
name: string
|
|
6
|
+
url: string
|
|
7
|
+
alive?: boolean
|
|
8
|
+
exec?: boolean
|
|
9
|
+
replace?: (code: string) => string
|
|
10
|
+
fetch?: typeof window.fetch
|
|
11
|
+
props?: Record<string, unknown>
|
|
12
|
+
attrs?: Record<string, unknown>
|
|
13
|
+
beforeLoad?: () => void
|
|
14
|
+
beforeMount?: () => void
|
|
15
|
+
afterMount?: () => void
|
|
16
|
+
beforeUnmount?: () => void
|
|
17
|
+
afterUnmount?: () => void
|
|
18
|
+
loadError?: (url: string, e: Error) => void
|
|
19
|
+
width?: string
|
|
20
|
+
height?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface EventBus {
|
|
24
|
+
$on: (event: string, callback: (...args: unknown[]) => void) => EventBus
|
|
25
|
+
$off: (event: string, callback: (...args: unknown[]) => void) => EventBus
|
|
26
|
+
$emit: (event: string, ...args: unknown[]) => EventBus
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class WujieReact extends Component<WujieReactProps> {
|
|
30
|
+
static bus: EventBus
|
|
31
|
+
static setupApp: (options: Record<string, unknown>) => void
|
|
32
|
+
static preloadApp: (options: Record<string, unknown>) => void
|
|
33
|
+
static destroyApp: (name: string) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default WujieReact
|
|
37
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { STORAGE_KEYS } from '@/constants'
|
|
2
|
+
|
|
3
|
+
export function getAccessToken(): string | null {
|
|
4
|
+
return localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function setAccessToken(token: string): void {
|
|
8
|
+
localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, token)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getRefreshToken(): string | null {
|
|
12
|
+
return localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function setRefreshToken(token: string): void {
|
|
16
|
+
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, token)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setTokens(accessToken: string, refreshToken: string): void {
|
|
20
|
+
setAccessToken(accessToken)
|
|
21
|
+
setRefreshToken(refreshToken)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function clearTokens(): void {
|
|
25
|
+
localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN)
|
|
26
|
+
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN)
|
|
27
|
+
localStorage.removeItem(STORAGE_KEYS.USER_INFO)
|
|
28
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import axios, { type AxiosError, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
|
|
2
|
+
import { message } from 'antd'
|
|
3
|
+
import { API_BASE_URL, TOKEN_CONFIG } from '@/constants'
|
|
4
|
+
import { getAccessToken, getRefreshToken, setTokens, clearTokens } from '@/utils/auth'
|
|
5
|
+
|
|
6
|
+
const request = axios.create({
|
|
7
|
+
baseURL: API_BASE_URL,
|
|
8
|
+
timeout: 15000,
|
|
9
|
+
headers: {
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// ---------- 请求拦截器 ----------
|
|
15
|
+
request.interceptors.request.use(
|
|
16
|
+
(config: InternalAxiosRequestConfig) => {
|
|
17
|
+
const token = getAccessToken()
|
|
18
|
+
if (token) {
|
|
19
|
+
config.headers.Authorization = `Bearer ${token}`
|
|
20
|
+
}
|
|
21
|
+
return config
|
|
22
|
+
},
|
|
23
|
+
(error: AxiosError) => Promise.reject(error),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
// ---------- Token 刷新锁 ----------
|
|
27
|
+
let isRefreshing = false
|
|
28
|
+
let pendingRequests: Array<(token: string) => void> = []
|
|
29
|
+
|
|
30
|
+
function onTokenRefreshed(newToken: string) {
|
|
31
|
+
pendingRequests.forEach((cb) => cb(newToken))
|
|
32
|
+
pendingRequests = []
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function refreshToken(): Promise<string> {
|
|
36
|
+
const refresh = getRefreshToken()
|
|
37
|
+
const res = await axios.post<{ data: { token: string; refreshToken: string } }>(
|
|
38
|
+
`${API_BASE_URL}/auth/refresh`,
|
|
39
|
+
{ refreshToken: refresh },
|
|
40
|
+
)
|
|
41
|
+
const { token, refreshToken: newRefresh } = res.data.data
|
|
42
|
+
setTokens(token, newRefresh)
|
|
43
|
+
return token
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------- 响应拦截器 ----------
|
|
47
|
+
request.interceptors.response.use(
|
|
48
|
+
(response: AxiosResponse) => response,
|
|
49
|
+
async (error: AxiosError<{ message?: string }>) => {
|
|
50
|
+
// 网络断开检测
|
|
51
|
+
if (!navigator.onLine) {
|
|
52
|
+
message.error('网络已断开,请检查网络连接')
|
|
53
|
+
return Promise.reject(error)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }
|
|
57
|
+
const status = error.response?.status
|
|
58
|
+
|
|
59
|
+
// 401: 自动刷新 token + 重放
|
|
60
|
+
if (status === 401 && !originalRequest._retry) {
|
|
61
|
+
if (isRefreshing) {
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
pendingRequests.push((newToken: string) => {
|
|
64
|
+
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
|
65
|
+
resolve(request(originalRequest))
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
originalRequest._retry = true
|
|
71
|
+
isRefreshing = true
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const newToken = await refreshToken()
|
|
75
|
+
isRefreshing = false
|
|
76
|
+
onTokenRefreshed(newToken)
|
|
77
|
+
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
|
78
|
+
return request(originalRequest)
|
|
79
|
+
} catch {
|
|
80
|
+
isRefreshing = false
|
|
81
|
+
pendingRequests = []
|
|
82
|
+
clearTokens()
|
|
83
|
+
window.location.href = '/login'
|
|
84
|
+
return Promise.reject(error)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 403: 无权限
|
|
89
|
+
if (status === 403) {
|
|
90
|
+
message.error('没有权限执行此操作')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 500/502/503: 服务异常
|
|
94
|
+
if (status && status >= 500) {
|
|
95
|
+
message.error('服务异常,请稍后重试')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return Promise.reject(error)
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
// ---------- 带重试的请求方法(用于关键接口) ----------
|
|
103
|
+
export async function requestWithRetry<T>(
|
|
104
|
+
config: Parameters<typeof request>[0],
|
|
105
|
+
retries = TOKEN_CONFIG.MAX_RETRY,
|
|
106
|
+
): Promise<T> {
|
|
107
|
+
for (let i = 0; i <= retries; i++) {
|
|
108
|
+
try {
|
|
109
|
+
return await request(config)
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (i === retries) throw err
|
|
112
|
+
await new Promise((r) => setTimeout(r, TOKEN_CONFIG.RETRY_DELAY * 2 ** i))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw new Error('Max retries exceeded')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default request
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import WujieReact from 'wujie-react'
|
|
3
|
+
import { Spin, Result, Button } from 'antd'
|
|
4
|
+
import { useTranslation } from 'react-i18next'
|
|
5
|
+
import { getSubAppConfig } from './config'
|
|
6
|
+
import { useSubAppProps } from './props'
|
|
7
|
+
|
|
8
|
+
interface SubAppProps {
|
|
9
|
+
/** 子应用名称,必须与 config 中注册的 name 一致 */
|
|
10
|
+
name: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 子应用加载超时时间 (ms) */
|
|
14
|
+
const LOAD_TIMEOUT = 15000
|
|
15
|
+
|
|
16
|
+
export default function SubApp({ name }: SubAppProps) {
|
|
17
|
+
const { t } = useTranslation('common')
|
|
18
|
+
const [loading, setLoading] = useState(true)
|
|
19
|
+
const [error, setError] = useState<string | null>(null)
|
|
20
|
+
const config = getSubAppConfig(name)
|
|
21
|
+
const subAppProps = useSubAppProps()
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
setLoading(false)
|
|
26
|
+
setError(t('subAppTimeout', { name }))
|
|
27
|
+
}, LOAD_TIMEOUT)
|
|
28
|
+
|
|
29
|
+
return () => clearTimeout(timer)
|
|
30
|
+
}, [name, t])
|
|
31
|
+
|
|
32
|
+
if (!config) {
|
|
33
|
+
return <Result status="error" title={t('subAppNotFound')} subTitle={`${name}`} />
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const handleLoadError = () => {
|
|
37
|
+
setLoading(false)
|
|
38
|
+
setError(t('subAppLoadError', { name }))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handleLoading = () => {
|
|
42
|
+
setLoading(false)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (error) {
|
|
46
|
+
return (
|
|
47
|
+
<Result
|
|
48
|
+
status="error"
|
|
49
|
+
title={t('subAppError')}
|
|
50
|
+
subTitle={error}
|
|
51
|
+
extra={
|
|
52
|
+
<Button type="primary" onClick={() => window.location.reload()}>
|
|
53
|
+
{t('retry')}
|
|
54
|
+
</Button>
|
|
55
|
+
}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Spin spinning={loading} tip={t('subAppLoading')} style={{ minHeight: 300 }}>
|
|
62
|
+
<WujieReact
|
|
63
|
+
name={config.name}
|
|
64
|
+
url={config.url}
|
|
65
|
+
alive={config.alive}
|
|
66
|
+
props={subAppProps as unknown as Record<string, unknown>}
|
|
67
|
+
loadError={handleLoadError}
|
|
68
|
+
beforeLoad={() => handleLoading()}
|
|
69
|
+
/>
|
|
70
|
+
</Spin>
|
|
71
|
+
)
|
|
72
|
+
}
|
package/src/wujie/bus.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import WujieReact from 'wujie-react'
|
|
2
|
+
import { BUS_EVENTS } from '@/constants/bus-events'
|
|
3
|
+
import type { Theme, Locale } from '@/store/appStore'
|
|
4
|
+
|
|
5
|
+
const { bus } = WujieReact
|
|
6
|
+
|
|
7
|
+
/** 向子应用广播语言切换 */
|
|
8
|
+
export function emitLocaleChange(locale: Locale) {
|
|
9
|
+
bus.$emit(BUS_EVENTS.LOCALE_CHANGE, locale)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** 向子应用广播主题切换 */
|
|
13
|
+
export function emitThemeChange(theme: Theme) {
|
|
14
|
+
bus.$emit(BUS_EVENTS.THEME_CHANGE, theme)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 向子应用广播站点切换 */
|
|
18
|
+
export function emitStationChange(stationId: string | null) {
|
|
19
|
+
bus.$emit(BUS_EVENTS.STATION_CHANGE, stationId)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 向子应用广播 Token 已刷新 */
|
|
23
|
+
export function emitTokenRefresh(token: string) {
|
|
24
|
+
bus.$emit(BUS_EVENTS.TOKEN_REFRESH, token)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 监听子应用 token-expired 事件 */
|
|
28
|
+
export function onTokenExpired(callback: () => void) {
|
|
29
|
+
bus.$on(BUS_EVENTS.TOKEN_EXPIRED, callback)
|
|
30
|
+
return () => bus.$off(BUS_EVENTS.TOKEN_EXPIRED, callback)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 监听子应用路由跳转事件 */
|
|
34
|
+
export function onNavigate(callback: (path: string) => void) {
|
|
35
|
+
const handler = (...args: unknown[]) => callback(args[0] as string)
|
|
36
|
+
bus.$on(BUS_EVENTS.NAVIGATE, handler)
|
|
37
|
+
return () => bus.$off(BUS_EVENTS.NAVIGATE, handler)
|
|
38
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SUB_APP_URLS } from '@/constants'
|
|
2
|
+
|
|
3
|
+
export interface SubAppConfig {
|
|
4
|
+
/** 子应用唯一名称 */
|
|
5
|
+
name: string
|
|
6
|
+
/** 子应用入口 URL */
|
|
7
|
+
url: string
|
|
8
|
+
/** 是否使用 alive 模式(保活) */
|
|
9
|
+
alive: boolean
|
|
10
|
+
/** 执行模式 */
|
|
11
|
+
exec?: boolean
|
|
12
|
+
/** 自定义 fetch */
|
|
13
|
+
fetch?: typeof fetch
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 子应用注册配置 */
|
|
17
|
+
export const subAppConfigs: SubAppConfig[] = [
|
|
18
|
+
{
|
|
19
|
+
name: 'sub-operation',
|
|
20
|
+
url: SUB_APP_URLS.OPERATION,
|
|
21
|
+
alive: true,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'sub-analysis',
|
|
25
|
+
url: SUB_APP_URLS.ANALYSIS,
|
|
26
|
+
alive: true,
|
|
27
|
+
},
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
/** 根据名称获取子应用配置 */
|
|
31
|
+
export function getSubAppConfig(name: string): SubAppConfig | undefined {
|
|
32
|
+
return subAppConfigs.find((app) => app.name === name)
|
|
33
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useUserStore } from '@/store/userStore'
|
|
3
|
+
import { useAppStore } from '@/store/appStore'
|
|
4
|
+
|
|
5
|
+
export interface SubAppPropsData {
|
|
6
|
+
token: string | null
|
|
7
|
+
userInfo: ReturnType<typeof useUserStore.getState>['userInfo']
|
|
8
|
+
permissions: string[]
|
|
9
|
+
theme: string
|
|
10
|
+
locale: string
|
|
11
|
+
currentStation: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 获取传递给子应用的 props(非响应式,用于一次性读取) */
|
|
15
|
+
export function getSubAppProps(): SubAppPropsData {
|
|
16
|
+
const userState = useUserStore.getState()
|
|
17
|
+
const appState = useAppStore.getState()
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
token: userState.token,
|
|
21
|
+
userInfo: userState.userInfo,
|
|
22
|
+
permissions: userState.permissions,
|
|
23
|
+
theme: appState.theme,
|
|
24
|
+
locale: appState.locale,
|
|
25
|
+
currentStation: appState.currentStation,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 响应式 hook:store 变更时自动返回最新 props,驱动 SubApp 重新传递 props 给子应用 */
|
|
30
|
+
export function useSubAppProps(): SubAppPropsData {
|
|
31
|
+
const token = useUserStore((s) => s.token)
|
|
32
|
+
const userInfo = useUserStore((s) => s.userInfo)
|
|
33
|
+
const permissions = useUserStore((s) => s.permissions)
|
|
34
|
+
const theme = useAppStore((s) => s.theme)
|
|
35
|
+
const locale = useAppStore((s) => s.locale)
|
|
36
|
+
const currentStation = useAppStore((s) => s.currentStation)
|
|
37
|
+
|
|
38
|
+
return useMemo(
|
|
39
|
+
() => ({ token, userInfo, permissions, theme, locale, currentStation }),
|
|
40
|
+
[token, userInfo, permissions, theme, locale, currentStation],
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
import { useAppStore } from '@/store/appStore'
|
|
4
|
+
import { useUserStore } from '@/store/userStore'
|
|
5
|
+
import {
|
|
6
|
+
emitLocaleChange,
|
|
7
|
+
emitThemeChange,
|
|
8
|
+
emitStationChange,
|
|
9
|
+
onTokenExpired,
|
|
10
|
+
onNavigate,
|
|
11
|
+
} from './bus'
|
|
12
|
+
import { refreshToken as refreshTokenApi } from '@/services/auth'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 主应用 bus 同步 hook
|
|
16
|
+
* - 监听 appStore 变更 → 广播给子应用
|
|
17
|
+
* - 监听子应用 bus 事件 → 响应处理
|
|
18
|
+
*/
|
|
19
|
+
export function useBusSync() {
|
|
20
|
+
const navigate = useNavigate()
|
|
21
|
+
const theme = useAppStore((s) => s.theme)
|
|
22
|
+
const locale = useAppStore((s) => s.locale)
|
|
23
|
+
const currentStation = useAppStore((s) => s.currentStation)
|
|
24
|
+
const setAuth = useUserStore((s) => s.setAuth)
|
|
25
|
+
const logout = useUserStore((s) => s.logout)
|
|
26
|
+
|
|
27
|
+
// 主题变更 → 广播给子应用
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
emitThemeChange(theme)
|
|
30
|
+
}, [theme])
|
|
31
|
+
|
|
32
|
+
// 语言变更 → 广播给子应用
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
emitLocaleChange(locale)
|
|
35
|
+
}, [locale])
|
|
36
|
+
|
|
37
|
+
// 站点变更 → 广播给子应用
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
emitStationChange(currentStation)
|
|
40
|
+
}, [currentStation])
|
|
41
|
+
|
|
42
|
+
// 监听子应用 token-expired → 刷新 token 或登出
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const unsubscribe = onTokenExpired(async () => {
|
|
45
|
+
try {
|
|
46
|
+
const res = await refreshTokenApi()
|
|
47
|
+
setAuth(res.data.data.accessToken, res.data.data.refreshToken)
|
|
48
|
+
} catch {
|
|
49
|
+
logout()
|
|
50
|
+
navigate('/login')
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
return () => {
|
|
54
|
+
unsubscribe()
|
|
55
|
+
}
|
|
56
|
+
}, [navigate, setAuth, logout])
|
|
57
|
+
|
|
58
|
+
// 监听子应用路由跳转
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const unsubscribe = onNavigate((path: string) => {
|
|
61
|
+
navigate(path)
|
|
62
|
+
})
|
|
63
|
+
return () => {
|
|
64
|
+
unsubscribe()
|
|
65
|
+
}
|
|
66
|
+
}, [navigate])
|
|
67
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "es2023",
|
|
5
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"types": ["vite/client"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"jsx": "react-jsx",
|
|
17
|
+
|
|
18
|
+
/* Strict */
|
|
19
|
+
"strict": true,
|
|
20
|
+
"noImplicitAny": true,
|
|
21
|
+
|
|
22
|
+
/* Linting */
|
|
23
|
+
"noUnusedLocals": true,
|
|
24
|
+
"noUnusedParameters": true,
|
|
25
|
+
"erasableSyntaxOnly": true,
|
|
26
|
+
"noFallthroughCasesInSwitch": true,
|
|
27
|
+
|
|
28
|
+
/* Path alias */
|
|
29
|
+
"paths": {
|
|
30
|
+
"@/*": ["./src/*"]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"include": ["src"]
|
|
34
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "es2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"erasableSyntaxOnly": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["vite.config.ts", "plugins"]
|
|
24
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import { mockDevServerPlugin } from 'vite-plugin-mock-dev-server'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
// https://vite.dev/config/
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [react(), mockDevServerPlugin()], //mockDevServerPlugin({ enabled: false })
|
|
9
|
+
resolve: {
|
|
10
|
+
alias: {
|
|
11
|
+
'@': path.resolve(__dirname, 'src'),
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
server: {
|
|
15
|
+
proxy: {
|
|
16
|
+
'^/api': { target: 'http://localhost:8080' },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
css: {
|
|
20
|
+
preprocessorOptions: {
|
|
21
|
+
less: {
|
|
22
|
+
javascriptEnabled: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
})
|