@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.
Files changed (43) hide show
  1. package/README.md +334 -67
  2. package/app/index.html +4 -3
  3. package/app/postcss.config.js +5 -0
  4. package/app/src/components/ThemeToggle.tsx +21 -0
  5. package/app/src/hooks/useTheme.ts +17 -0
  6. package/app/src/layouts/dashboard.tsx +259 -0
  7. package/app/src/main.tsx +121 -0
  8. package/app/src/pages/dashboard-bots.tsx +198 -0
  9. package/app/src/pages/dashboard-home.tsx +301 -0
  10. package/app/src/pages/dashboard-logs.tsx +298 -0
  11. package/app/src/pages/dashboard-plugin-detail.tsx +360 -0
  12. package/app/src/pages/dashboard-plugins.tsx +166 -0
  13. package/app/src/style.css +1105 -0
  14. package/app/src/theme/index.ts +92 -0
  15. package/app/tailwind.config.js +70 -0
  16. package/app/tsconfig.json +5 -0
  17. package/dist/index.js +15 -3
  18. package/package.json +20 -7
  19. package/src/index.ts +19 -3
  20. package/src/router/index.tsx +55 -0
  21. package/src/store/index.ts +111 -0
  22. package/src/store/reducers/index.ts +16 -0
  23. package/src/store/reducers/route.ts +122 -0
  24. package/src/store/reducers/script.ts +103 -0
  25. package/src/store/reducers/ui.ts +31 -0
  26. package/src/types.ts +11 -17
  27. package/src/websocket/index.ts +193 -0
  28. package/src/websocket/useWebSocket.ts +42 -0
  29. package/app/components.d.ts +0 -33
  30. package/app/src/App.vue +0 -7
  31. package/app/src/main.ts +0 -127
  32. package/app/src/pages/$.vue +0 -899
  33. package/app/src/pages/404.vue +0 -11
  34. package/app/src/pages/contexts/overview.vue +0 -177
  35. package/app/src/pages/dashboard.vue +0 -323
  36. package/app/src/pages/plugins/installed.vue +0 -734
  37. package/app/src/pages/system/status.vue +0 -241
  38. package/app/src/services/api.ts +0 -155
  39. package/app/src/styles/README.md +0 -202
  40. package/app/src/styles/common.css +0 -0
  41. package/global.d.ts +0 -19
  42. package/src/router.ts +0 -44
  43. 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
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "baseUrl": ".",
4
+ "target": "ES2020",
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "moduleResolution": "bundler",
7
+ "module": "ESNext",
8
+ "jsx": "react-jsx",
4
9
  "paths": {
5
10
  "@/*": ["src/*"],
6
11
  "@zhin.js/client": [
package/dist/index.js CHANGED
@@ -1,4 +1,16 @@
1
- export * from './router.js';
2
- export * from './types.js';
3
- export * from './store.js';
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.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
- "pinia": "latest",
16
- "primevue": "^4.3.7",
17
- "primeicons": "latest",
18
- "@primeuix/themes": "^1.2.3",
19
- "vue-router": "latest"
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
- export * from './router.js';
2
- export * from './types.js';
3
- export * from './store.js';
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
+