@zhin.js/client 1.0.0 → 1.0.2
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 +334 -67
- package/app/index.html +4 -3
- package/app/postcss.config.js +5 -0
- package/app/src/components/ThemeToggle.tsx +21 -0
- package/app/src/hooks/useTheme.ts +17 -0
- package/app/src/layouts/dashboard.tsx +259 -0
- package/app/src/main.tsx +121 -0
- package/app/src/pages/dashboard-bots.tsx +198 -0
- package/app/src/pages/dashboard-home.tsx +301 -0
- package/app/src/pages/dashboard-logs.tsx +298 -0
- package/app/src/pages/dashboard-plugin-detail.tsx +360 -0
- package/app/src/pages/dashboard-plugins.tsx +166 -0
- package/app/src/style.css +1105 -0
- package/app/src/theme/index.ts +92 -0
- package/app/tailwind.config.js +70 -0
- package/app/tsconfig.json +5 -0
- package/dist/index.js +15 -3
- package/package.json +20 -7
- package/src/index.ts +19 -3
- package/src/router/index.tsx +55 -0
- package/src/store/index.ts +111 -0
- package/src/store/reducers/index.ts +16 -0
- package/src/store/reducers/route.ts +122 -0
- package/src/store/reducers/script.ts +103 -0
- package/src/store/reducers/ui.ts +31 -0
- package/src/types.ts +11 -17
- package/src/websocket/index.ts +193 -0
- package/src/websocket/useWebSocket.ts +42 -0
- package/app/components.d.ts +0 -33
- package/app/src/App.vue +0 -7
- package/app/src/main.ts +0 -127
- package/app/src/pages/$.vue +0 -899
- package/app/src/pages/404.vue +0 -11
- package/app/src/pages/contexts/overview.vue +0 -177
- package/app/src/pages/dashboard.vue +0 -323
- package/app/src/pages/plugins/installed.vue +0 -734
- package/app/src/pages/system/status.vue +0 -241
- package/app/src/services/api.ts +0 -155
- package/app/src/styles/README.md +0 -202
- package/app/src/styles/common.css +0 -0
- package/global.d.ts +0 -19
- package/src/router.ts +0 -44
- package/src/store.ts +0 -53
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Theme configuration
|
|
2
|
+
export const themes = {
|
|
3
|
+
light: {
|
|
4
|
+
background: '0 0% 100%',
|
|
5
|
+
foreground: '222.2 84% 4.9%',
|
|
6
|
+
card: '0 0% 100%',
|
|
7
|
+
'card-foreground': '222.2 84% 4.9%',
|
|
8
|
+
popover: '0 0% 100%',
|
|
9
|
+
'popover-foreground': '222.2 84% 4.9%',
|
|
10
|
+
primary: '221.2 83.2% 53.3%',
|
|
11
|
+
'primary-foreground': '210 40% 98%',
|
|
12
|
+
secondary: '210 40% 96.1%',
|
|
13
|
+
'secondary-foreground': '222.2 47.4% 11.2%',
|
|
14
|
+
muted: '210 40% 96.1%',
|
|
15
|
+
'muted-foreground': '215.4 16.3% 46.9%',
|
|
16
|
+
accent: '210 40% 96.1%',
|
|
17
|
+
'accent-foreground': '222.2 47.4% 11.2%',
|
|
18
|
+
destructive: '0 84.2% 60.2%',
|
|
19
|
+
'destructive-foreground': '210 40% 98%',
|
|
20
|
+
border: '214.3 31.8% 91.4%',
|
|
21
|
+
input: '214.3 31.8% 91.4%',
|
|
22
|
+
ring: '221.2 83.2% 53.3%',
|
|
23
|
+
radius: '0.5rem',
|
|
24
|
+
},
|
|
25
|
+
dark: {
|
|
26
|
+
background: '222.2 84% 4.9%',
|
|
27
|
+
foreground: '210 40% 98%',
|
|
28
|
+
card: '222.2 84% 4.9%',
|
|
29
|
+
'card-foreground': '210 40% 98%',
|
|
30
|
+
popover: '222.2 84% 4.9%',
|
|
31
|
+
'popover-foreground': '210 40% 98%',
|
|
32
|
+
primary: '217.2 91.2% 59.8%',
|
|
33
|
+
'primary-foreground': '222.2 47.4% 11.2%',
|
|
34
|
+
secondary: '217.2 32.6% 17.5%',
|
|
35
|
+
'secondary-foreground': '210 40% 98%',
|
|
36
|
+
muted: '217.2 32.6% 17.5%',
|
|
37
|
+
'muted-foreground': '215 20.2% 65.1%',
|
|
38
|
+
accent: '217.2 32.6% 17.5%',
|
|
39
|
+
'accent-foreground': '210 40% 98%',
|
|
40
|
+
destructive: '0 62.8% 30.6%',
|
|
41
|
+
'destructive-foreground': '210 40% 98%',
|
|
42
|
+
border: '217.2 32.6% 17.5%',
|
|
43
|
+
input: '217.2 32.6% 17.5%',
|
|
44
|
+
ring: '224.3 76.3% 48%',
|
|
45
|
+
radius: '0.5rem',
|
|
46
|
+
},
|
|
47
|
+
} as const
|
|
48
|
+
|
|
49
|
+
export type Theme = keyof typeof themes
|
|
50
|
+
export type ThemeColors = typeof themes.light
|
|
51
|
+
|
|
52
|
+
// Apply theme to document
|
|
53
|
+
export function applyTheme(theme: Theme) {
|
|
54
|
+
const root = document.documentElement
|
|
55
|
+
const colors = themes[theme]
|
|
56
|
+
|
|
57
|
+
// Remove old theme class
|
|
58
|
+
root.classList.remove('light', 'dark')
|
|
59
|
+
// Add new theme class
|
|
60
|
+
root.classList.add(theme)
|
|
61
|
+
|
|
62
|
+
// Apply CSS variables
|
|
63
|
+
Object.entries(colors).forEach(([key, value]) => {
|
|
64
|
+
root.style.setProperty(`--${key}`, value)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Save to localStorage
|
|
68
|
+
localStorage.setItem('theme', theme)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Get current theme from localStorage or system preference
|
|
72
|
+
export function getInitialTheme(): Theme {
|
|
73
|
+
const stored = localStorage.getItem('theme') as Theme | null
|
|
74
|
+
if (stored && (stored === 'light' || stored === 'dark')) {
|
|
75
|
+
return stored
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check system preference
|
|
79
|
+
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
80
|
+
return 'dark'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return 'light'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Initialize theme on app load
|
|
87
|
+
export function initializeTheme() {
|
|
88
|
+
const theme = getInitialTheme()
|
|
89
|
+
applyTheme(theme)
|
|
90
|
+
return theme
|
|
91
|
+
}
|
|
92
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
const cwd = process.cwd();
|
|
3
|
+
export default {
|
|
4
|
+
content: [
|
|
5
|
+
"./index.html",
|
|
6
|
+
'./src/**/*.{js,ts,jsx,tsx,mdx}',
|
|
7
|
+
`${cwd}/**/radix-ui/**/*.{js,ts,jsx,tsx}`,
|
|
8
|
+
`${cwd}/**/@radix-ui/themes/**/*.{js,ts,jsx,tsx}`,
|
|
9
|
+
],
|
|
10
|
+
theme: {
|
|
11
|
+
extend: {
|
|
12
|
+
colors: {
|
|
13
|
+
border: 'hsl(var(--border))',
|
|
14
|
+
input: 'hsl(var(--input))',
|
|
15
|
+
ring: 'hsl(var(--ring))',
|
|
16
|
+
background: 'hsl(var(--background))',
|
|
17
|
+
foreground: 'hsl(var(--foreground))',
|
|
18
|
+
primary: {
|
|
19
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
20
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
21
|
+
},
|
|
22
|
+
secondary: {
|
|
23
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
24
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
25
|
+
},
|
|
26
|
+
destructive: {
|
|
27
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
28
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
29
|
+
},
|
|
30
|
+
muted: {
|
|
31
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
32
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
33
|
+
},
|
|
34
|
+
accent: {
|
|
35
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
36
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
37
|
+
},
|
|
38
|
+
popover: {
|
|
39
|
+
DEFAULT: 'hsl(var(--popover))',
|
|
40
|
+
foreground: 'hsl(var(--popover-foreground))',
|
|
41
|
+
},
|
|
42
|
+
card: {
|
|
43
|
+
DEFAULT: 'hsl(var(--card))',
|
|
44
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
borderRadius: {
|
|
48
|
+
lg: 'var(--radius)',
|
|
49
|
+
md: 'calc(var(--radius) - 2px)',
|
|
50
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
51
|
+
},
|
|
52
|
+
keyframes: {
|
|
53
|
+
"accordion-down": {
|
|
54
|
+
from: { height: "0" },
|
|
55
|
+
to: { height: "var(--radix-accordion-content-height)" },
|
|
56
|
+
},
|
|
57
|
+
"accordion-up": {
|
|
58
|
+
from: { height: "var(--radix-accordion-content-height)" },
|
|
59
|
+
to: { height: "0" },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
animation: {
|
|
63
|
+
"accordion-down": "accordion-down 0.2s ease-out",
|
|
64
|
+
"accordion-up": "accordion-up 0.2s ease-out",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
darkMode: "class",
|
|
69
|
+
plugins: [],
|
|
70
|
+
}
|
package/app/tsconfig.json
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
// Types
|
|
4
|
+
export * from './types';
|
|
5
|
+
// Redux Store
|
|
6
|
+
export * from './store';
|
|
7
|
+
// Router
|
|
8
|
+
export * from './router';
|
|
9
|
+
export * as Icons from 'lucide-react';
|
|
10
|
+
// WebSocket
|
|
11
|
+
export * from './websocket';
|
|
12
|
+
export { useWebSocket } from './websocket/useWebSocket';
|
|
13
|
+
export function cn(...inputs) {
|
|
14
|
+
return twMerge(clsx(inputs));
|
|
15
|
+
}
|
|
4
16
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/client",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Zhin 客户端",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -12,18 +12,31 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
15
|
+
"@reduxjs/toolkit": "^2.9.0",
|
|
16
|
+
"@tailwindcss/postcss": "^4.1.11",
|
|
17
|
+
"clsx": "^2.1.1",
|
|
18
|
+
"react": "^19.2.0",
|
|
19
|
+
"react-dom": "^19.2.0",
|
|
20
|
+
"react-redux": "9.2.0",
|
|
21
|
+
"react-router": "7.0.0",
|
|
22
|
+
"redux-persist": "6.0.0",
|
|
23
|
+
"tailwind-merge": "^3.3.1",
|
|
24
|
+
"tailwindcss": "latest"
|
|
20
25
|
},
|
|
21
26
|
"files": [
|
|
22
27
|
"lib",
|
|
23
28
|
"src",
|
|
24
|
-
"global.d.ts",
|
|
25
29
|
"app"
|
|
26
30
|
],
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"radix-ui": "^1.4.3",
|
|
33
|
+
"@radix-ui/themes": "^3.2.1",
|
|
34
|
+
"lucide-react": "^0.469.0",
|
|
35
|
+
"@types/events": "^3.0.3",
|
|
36
|
+
"@types/node": "^24.7.1",
|
|
37
|
+
"@types/react": "^19.2.2",
|
|
38
|
+
"@types/react-dom": "^19.2.1"
|
|
39
|
+
},
|
|
27
40
|
"scripts": {
|
|
28
41
|
"build": "tsc",
|
|
29
42
|
"clean": "rm -rf dist"
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { type ClassValue, clsx } from 'clsx'
|
|
2
|
+
import { twMerge } from 'tailwind-merge'
|
|
3
|
+
// Types
|
|
4
|
+
export * from './types'
|
|
5
|
+
|
|
6
|
+
// Redux Store
|
|
7
|
+
export * from './store'
|
|
8
|
+
|
|
9
|
+
// Router
|
|
10
|
+
export * from './router'
|
|
11
|
+
export * as Icons from 'lucide-react'
|
|
12
|
+
|
|
13
|
+
// WebSocket
|
|
14
|
+
export * from './websocket'
|
|
15
|
+
export { useWebSocket } from './websocket/useWebSocket'
|
|
16
|
+
|
|
17
|
+
export function cn(...inputs: ClassValue[]) {
|
|
18
|
+
return twMerge(clsx(inputs))
|
|
19
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { createBrowserRouter, RouterProvider as ReactRouterProvider, RouteObject, Outlet} from 'react-router'
|
|
3
|
+
import { store, addRoute, removeRoute, updateRoute, clearRoutes, RouteMenuItem, useSelector } from '../store'
|
|
4
|
+
export { useOutlet, Outlet, Link, useNavigate, useParams } from 'react-router'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export const addPage = (route: RouteMenuItem) => store.dispatch(addRoute(route))
|
|
10
|
+
export const removePage = (path: string) => store.dispatch(removeRoute(path))
|
|
11
|
+
export const updatePage = (path: string, route: RouteMenuItem) => store.dispatch(updateRoute({
|
|
12
|
+
path,
|
|
13
|
+
updates: route
|
|
14
|
+
}))
|
|
15
|
+
export const getPage = (path: string) => store.getState().route.routes.find(route => route.path === path)
|
|
16
|
+
export const getAllPages = () => store.getState().route.routes
|
|
17
|
+
export const clearPages = () => store.dispatch(clearRoutes())
|
|
18
|
+
|
|
19
|
+
// 动态路由组件(从 Redux store 读取)
|
|
20
|
+
export function DynamicRouter() {
|
|
21
|
+
const storeRoutes = useSelector((state) => state.route.routes)
|
|
22
|
+
console.log(storeRoutes)
|
|
23
|
+
const router = useMemo(() => {
|
|
24
|
+
// 递归转换路由(支持多层嵌套)
|
|
25
|
+
const convertRoute = (route: RouteMenuItem): RouteObject => {
|
|
26
|
+
const routeObj: RouteObject = {
|
|
27
|
+
path: route.path,
|
|
28
|
+
element: route.element,
|
|
29
|
+
Component: route.Component,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 递归处理子路由
|
|
33
|
+
if (route.children && route.children.length > 0) {
|
|
34
|
+
routeObj.children = route.children.map((child: RouteMenuItem) => convertRoute(child))
|
|
35
|
+
}
|
|
36
|
+
return routeObj
|
|
37
|
+
}
|
|
38
|
+
const routeObjects: RouteObject[] = storeRoutes.map(convertRoute)
|
|
39
|
+
const defaultRoutes: RouteObject[] = [
|
|
40
|
+
{
|
|
41
|
+
path: '/',
|
|
42
|
+
element: <Outlet />,
|
|
43
|
+
children: routeObjects,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
path: '*',
|
|
47
|
+
element: <div>404 - Page Not Found</div>,
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
return createBrowserRouter(defaultRoutes)
|
|
52
|
+
}, [storeRoutes])
|
|
53
|
+
|
|
54
|
+
return <ReactRouterProvider router={router} />
|
|
55
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {configureStore, Reducer, Action,combineReducers} from '@reduxjs/toolkit'
|
|
2
|
+
import {
|
|
3
|
+
FLUSH,
|
|
4
|
+
PAUSE,
|
|
5
|
+
PERSIST,
|
|
6
|
+
persistReducer,
|
|
7
|
+
persistStore,
|
|
8
|
+
PURGE,
|
|
9
|
+
REGISTER,
|
|
10
|
+
REHYDRATE,
|
|
11
|
+
createTransform,
|
|
12
|
+
} from 'redux-persist';
|
|
13
|
+
import storage from 'redux-persist/es/storage';
|
|
14
|
+
import {reducers, Reducers } from './reducers';
|
|
15
|
+
import { useDispatch as useReduxDispatch, TypedUseSelectorHook, useSelector as useReduxSelector } from 'react-redux';
|
|
16
|
+
|
|
17
|
+
// 创建 transform 过滤不可序列化的字段
|
|
18
|
+
const routeTransform = createTransform(
|
|
19
|
+
(inboundState: any) => {
|
|
20
|
+
const { routes, ...rest } = inboundState
|
|
21
|
+
return rest
|
|
22
|
+
},
|
|
23
|
+
(outboundState: any) => {
|
|
24
|
+
return { ...outboundState, routes: [] }
|
|
25
|
+
},
|
|
26
|
+
{ whitelist: ['route'] }
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const scriptTransform = createTransform(
|
|
30
|
+
(inboundState: any) => {
|
|
31
|
+
const { entries, loadedScripts, ...rest } = inboundState
|
|
32
|
+
return rest
|
|
33
|
+
},
|
|
34
|
+
(outboundState: any) => {
|
|
35
|
+
return { ...outboundState, entries: [], loadedScripts: [] }
|
|
36
|
+
},
|
|
37
|
+
{ whitelist: ['script'] }
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const persistConfig: any = {
|
|
41
|
+
key: 'root',
|
|
42
|
+
storage: storage,
|
|
43
|
+
transforms: [routeTransform, scriptTransform],
|
|
44
|
+
}
|
|
45
|
+
const persistedReducer = persistReducer(persistConfig, combineReducers(reducers)) as any
|
|
46
|
+
export const store = configureStore<Reducers>({
|
|
47
|
+
reducer:persistedReducer as Reducer,
|
|
48
|
+
middleware: (getDefaultMiddleware) =>
|
|
49
|
+
getDefaultMiddleware({
|
|
50
|
+
serializableCheck: {
|
|
51
|
+
ignoredActions: [
|
|
52
|
+
FLUSH,
|
|
53
|
+
REHYDRATE,
|
|
54
|
+
PAUSE,
|
|
55
|
+
PERSIST,
|
|
56
|
+
PURGE,
|
|
57
|
+
REGISTER,
|
|
58
|
+
// 忽略包含 React 组件的路由 actions
|
|
59
|
+
'route/addRoute',
|
|
60
|
+
'route/updateRoute',
|
|
61
|
+
'route/setRoutes',
|
|
62
|
+
],
|
|
63
|
+
// 忽略 state 中的 routes 字段
|
|
64
|
+
ignoredPaths: ['route.routes'],
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
})
|
|
68
|
+
export const persistor = persistStore(store);
|
|
69
|
+
|
|
70
|
+
// 将 store 挂载到 window 以供 DynamicRouter 使用
|
|
71
|
+
if (typeof window !== 'undefined') {
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
window.__REDUX_STORE__ = store
|
|
74
|
+
}
|
|
75
|
+
export function addReducer<T,A extends Action>(name:keyof Reducers,reducer:Reducer<T,A>) {
|
|
76
|
+
reducers[name] = reducer
|
|
77
|
+
const newPersistedReducer = persistReducer(persistConfig, combineReducers(reducers)) as any
|
|
78
|
+
store.replaceReducer(newPersistedReducer as Reducer)
|
|
79
|
+
}
|
|
80
|
+
export type RootState = ReturnType<typeof store.getState>
|
|
81
|
+
export type AppDispatch = typeof store.dispatch
|
|
82
|
+
export const useDispatch:()=>AppDispatch = useReduxDispatch
|
|
83
|
+
export const useSelector:TypedUseSelectorHook<RootState> = useReduxSelector
|
|
84
|
+
|
|
85
|
+
// 导出 UI actions
|
|
86
|
+
export {
|
|
87
|
+
toggleSidebar,
|
|
88
|
+
setSidebarOpen,
|
|
89
|
+
setActiveMenu
|
|
90
|
+
} from './reducers/ui'
|
|
91
|
+
|
|
92
|
+
// 导出 Route actions
|
|
93
|
+
export {
|
|
94
|
+
addRoute,
|
|
95
|
+
removeRoute,
|
|
96
|
+
updateRoute,
|
|
97
|
+
setRoutes,
|
|
98
|
+
clearRoutes
|
|
99
|
+
} from './reducers/route'
|
|
100
|
+
|
|
101
|
+
// 导出 Script actions 和 thunks
|
|
102
|
+
export {
|
|
103
|
+
syncEntries,
|
|
104
|
+
addEntry,
|
|
105
|
+
removeEntry,
|
|
106
|
+
loadScript,
|
|
107
|
+
loadScripts,
|
|
108
|
+
unloadScript
|
|
109
|
+
} from './reducers/script'
|
|
110
|
+
|
|
111
|
+
export type { RouteMenuItem } from './reducers/route'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Reducer } from "@reduxjs/toolkit"
|
|
2
|
+
import ui from "./ui"
|
|
3
|
+
import route from "./route"
|
|
4
|
+
import script from "./script"
|
|
5
|
+
|
|
6
|
+
export interface Reducers {
|
|
7
|
+
ui: ReturnType<typeof ui>
|
|
8
|
+
route: ReturnType<typeof route>
|
|
9
|
+
script: ReturnType<typeof script>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const reducers: Record<keyof Reducers, Reducer<any, any>> = {
|
|
13
|
+
ui,
|
|
14
|
+
route,
|
|
15
|
+
script
|
|
16
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
|
|
2
|
+
import { ComponentType, ReactNode } from "react"
|
|
3
|
+
|
|
4
|
+
// 路由菜单项接口
|
|
5
|
+
export interface RouteMenuItem {
|
|
6
|
+
key: string
|
|
7
|
+
path: string
|
|
8
|
+
title: string
|
|
9
|
+
index?: boolean
|
|
10
|
+
icon?: ReactNode
|
|
11
|
+
element?: ReactNode
|
|
12
|
+
Component?: ComponentType
|
|
13
|
+
children?: RouteMenuItem[]
|
|
14
|
+
meta?: {
|
|
15
|
+
hideInMenu?: boolean
|
|
16
|
+
requiresAuth?: boolean
|
|
17
|
+
order?: number
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RouteState {
|
|
22
|
+
routes: RouteMenuItem[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const initialState: RouteState = {
|
|
26
|
+
routes: []
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 辅助函数:查找最佳父路由
|
|
30
|
+
const findBestParent = (routes: RouteMenuItem[], fullPath: string): RouteMenuItem | null => {
|
|
31
|
+
const segments = fullPath.split('/').filter(Boolean)
|
|
32
|
+
|
|
33
|
+
for (let i = segments.length - 1; i > 0; i--) {
|
|
34
|
+
const parentPath = '/' + segments.slice(0, i).join('/')
|
|
35
|
+
const parent = routes.find(r => r.path === parentPath)
|
|
36
|
+
if (parent) return parent
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return routes.find(r=>r.path==='/')||null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 辅助函数:计算相对路径
|
|
43
|
+
const calculateRelativePath = (parentPath: string, fullPath: string): string => {
|
|
44
|
+
if (parentPath === '/') return fullPath
|
|
45
|
+
return fullPath.replace(parentPath + '/', '')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const routeSlice = createSlice({
|
|
49
|
+
name: 'route',
|
|
50
|
+
initialState,
|
|
51
|
+
reducers: {
|
|
52
|
+
addRoute: (state, action: PayloadAction<RouteMenuItem>) => {
|
|
53
|
+
const route = action.payload
|
|
54
|
+
const parent = findBestParent(state.routes, route.path)
|
|
55
|
+
|
|
56
|
+
if (parent) {
|
|
57
|
+
if (!parent.children) parent.children = []
|
|
58
|
+
const relativePath = calculateRelativePath(parent.path, route.path)
|
|
59
|
+
parent.children.push({ ...route, path: relativePath })
|
|
60
|
+
} else {
|
|
61
|
+
state.routes.push(route)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
state.routes.sort((a, b) => (a.meta?.order || 999) - (b.meta?.order || 999))
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
removeRoute: (state, action: PayloadAction<string>) => {
|
|
68
|
+
const path = action.payload
|
|
69
|
+
|
|
70
|
+
const removeFromArray = (routes: RouteMenuItem[]): boolean => {
|
|
71
|
+
const index = routes.findIndex(r => r.path === path)
|
|
72
|
+
if (index >= 0) {
|
|
73
|
+
routes.splice(index, 1)
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const route of routes) {
|
|
78
|
+
if (route.children && removeFromArray(route.children)) {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
removeFromArray(state.routes)
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
updateRoute: (state, action: PayloadAction<{ path: string; updates: Partial<RouteMenuItem> }>) => {
|
|
89
|
+
const { path, updates } = action.payload
|
|
90
|
+
|
|
91
|
+
const updateInArray = (routes: RouteMenuItem[]): boolean => {
|
|
92
|
+
const index = routes.findIndex(r => r.path === path)
|
|
93
|
+
if (index >= 0) {
|
|
94
|
+
routes[index] = { ...routes[index], ...updates }
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const route of routes) {
|
|
99
|
+
if (route.children && updateInArray(route.children)) {
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
updateInArray(state.routes)
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
setRoutes: (state, action: PayloadAction<RouteMenuItem[]>) => {
|
|
110
|
+
state.routes = action.payload
|
|
111
|
+
state.routes.sort((a, b) => (a.meta?.order || 999) - (b.meta?.order || 999))
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
clearRoutes: (state) => {
|
|
115
|
+
state.routes = []
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
export const { addRoute, removeRoute, updateRoute, setRoutes, clearRoutes } = routeSlice.actions
|
|
121
|
+
export default routeSlice.reducer
|
|
122
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit"
|
|
2
|
+
|
|
3
|
+
export interface ScriptState {
|
|
4
|
+
entries: string[]
|
|
5
|
+
loadedScripts: string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const initialState: ScriptState = {
|
|
9
|
+
entries: [],
|
|
10
|
+
loadedScripts: []
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// AsyncThunk: 加载单个脚本
|
|
14
|
+
export const loadScript = createAsyncThunk(
|
|
15
|
+
'script/loadScript',
|
|
16
|
+
async (src: string) => {
|
|
17
|
+
return new Promise<string>((resolve, reject) => {
|
|
18
|
+
const script = document.createElement('script')
|
|
19
|
+
script.type = 'module'
|
|
20
|
+
script.src = src
|
|
21
|
+
script.dataset.dynamicEntry = 'true'
|
|
22
|
+
|
|
23
|
+
script.onload = () => resolve(src)
|
|
24
|
+
script.onerror = (error) => {
|
|
25
|
+
console.error('[Script] Load failed:', src)
|
|
26
|
+
reject(error)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
document.body.appendChild(script)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// AsyncThunk: 批量加载脚本
|
|
35
|
+
export const loadScripts = createAsyncThunk(
|
|
36
|
+
'script/loadScripts',
|
|
37
|
+
async (entries: string[], { dispatch }) => {
|
|
38
|
+
const results = await Promise.allSettled(
|
|
39
|
+
entries.map(entry => dispatch(loadScript(entry)).unwrap())
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return results
|
|
43
|
+
.filter((r): r is PromiseFulfilledResult<string> => r.status === 'fulfilled')
|
|
44
|
+
.map(r => r.value)
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// AsyncThunk: 卸载脚本
|
|
49
|
+
export const unloadScript = createAsyncThunk(
|
|
50
|
+
'script/unloadScript',
|
|
51
|
+
async (src: string) => {
|
|
52
|
+
const scripts = document.querySelectorAll(`script[src="${src}"][data-dynamic-entry="true"]`)
|
|
53
|
+
scripts.forEach(script => script.remove())
|
|
54
|
+
return src
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
export const scriptSlice = createSlice({
|
|
59
|
+
name: 'script',
|
|
60
|
+
initialState,
|
|
61
|
+
reducers: {
|
|
62
|
+
syncEntries: (state, action: PayloadAction<string[]>) => {
|
|
63
|
+
state.entries = action.payload
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
addEntry: (state, action: PayloadAction<string>) => {
|
|
67
|
+
if (!state.entries.includes(action.payload)) {
|
|
68
|
+
state.entries.push(action.payload)
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
removeEntry: (state, action: PayloadAction<string>) => {
|
|
73
|
+
state.entries = state.entries.filter(e => e !== action.payload)
|
|
74
|
+
state.loadedScripts = state.loadedScripts.filter(s => s !== action.payload)
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
extraReducers: (builder) => {
|
|
78
|
+
// loadScript 成功
|
|
79
|
+
builder.addCase(loadScript.fulfilled, (state, action) => {
|
|
80
|
+
if (!state.loadedScripts.includes(action.payload)) {
|
|
81
|
+
state.loadedScripts.push(action.payload)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// loadScripts 成功
|
|
86
|
+
builder.addCase(loadScripts.fulfilled, (state, action) => {
|
|
87
|
+
action.payload.forEach(src => {
|
|
88
|
+
if (!state.loadedScripts.includes(src)) {
|
|
89
|
+
state.loadedScripts.push(src)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// unloadScript 成功
|
|
95
|
+
builder.addCase(unloadScript.fulfilled, (state, action) => {
|
|
96
|
+
state.loadedScripts = state.loadedScripts.filter(s => s !== action.payload)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
export const { syncEntries, addEntry, removeEntry } = scriptSlice.actions
|
|
102
|
+
export default scriptSlice.reducer
|
|
103
|
+
|