@zhin.js/client 1.0.4 → 1.0.6
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 +23 -27
- package/{src → client}/index.ts +0 -1
- package/{src → client}/store/reducers/route.ts +1 -1
- package/{src → client}/store/reducers/script.ts +1 -1
- package/{src → client}/store/reducers/ui.ts +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -0
- package/dist/router/index.d.ts +25 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +49 -0
- package/dist/router/index.js.map +1 -0
- package/dist/store/index.d.ts +19 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +67 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/reducers/config.d.ts +54 -0
- package/dist/store/reducers/config.d.ts.map +1 -0
- package/dist/store/reducers/config.js +78 -0
- package/dist/store/reducers/config.js.map +1 -0
- package/dist/store/reducers/index.d.ts +13 -0
- package/dist/store/reducers/index.d.ts.map +1 -0
- package/dist/store/reducers/index.js +11 -0
- package/dist/store/reducers/index.js.map +1 -0
- package/dist/store/reducers/route.d.ts +26 -0
- package/dist/store/reducers/route.d.ts.map +1 -0
- package/dist/store/reducers/route.js +85 -0
- package/dist/store/reducers/route.js.map +1 -0
- package/dist/store/reducers/script.d.ts +11 -0
- package/dist/store/reducers/script.d.ts.map +1 -0
- package/dist/store/reducers/script.js +74 -0
- package/dist/store/reducers/script.js.map +1 -0
- package/dist/store/reducers/ui.d.ts +8 -0
- package/dist/store/reducers/ui.d.ts.map +1 -0
- package/dist/store/reducers/ui.js +23 -0
- package/dist/store/reducers/ui.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/websocket/hooks.d.ts +55 -0
- package/dist/websocket/hooks.d.ts.map +1 -0
- package/dist/websocket/hooks.js +225 -0
- package/dist/websocket/hooks.js.map +1 -0
- package/dist/websocket/index.d.ts +13 -0
- package/dist/websocket/index.d.ts.map +1 -0
- package/dist/websocket/index.js +31 -0
- package/dist/websocket/index.js.map +1 -0
- package/dist/websocket/instance.d.ts +18 -0
- package/dist/websocket/instance.d.ts.map +1 -0
- package/dist/websocket/instance.js +39 -0
- package/dist/websocket/instance.js.map +1 -0
- package/dist/websocket/manager.d.ts +110 -0
- package/dist/websocket/manager.d.ts.map +1 -0
- package/dist/websocket/manager.js +341 -0
- package/dist/websocket/manager.js.map +1 -0
- package/dist/websocket/messageHandler.d.ts +48 -0
- package/dist/websocket/messageHandler.d.ts.map +1 -0
- package/dist/websocket/messageHandler.js +140 -0
- package/dist/websocket/messageHandler.js.map +1 -0
- package/dist/websocket/types.d.ts +125 -0
- package/dist/websocket/types.d.ts.map +1 -0
- package/dist/websocket/types.js +43 -0
- package/dist/websocket/types.js.map +1 -0
- package/package.json +12 -18
- package/app/index.html +0 -13
- package/app/postcss.config.js +0 -5
- package/app/src/components/PluginConfigForm/BasicFieldRenderers.tsx +0 -253
- package/app/src/components/PluginConfigForm/CollectionFieldRenderers.tsx +0 -261
- package/app/src/components/PluginConfigForm/CompositeFieldRenderers.tsx +0 -105
- package/app/src/components/PluginConfigForm/FieldRenderer.tsx +0 -110
- package/app/src/components/PluginConfigForm/NestedFieldRenderer.tsx +0 -95
- package/app/src/components/PluginConfigForm/index.tsx +0 -237
- package/app/src/components/PluginConfigForm/types.ts +0 -46
- package/app/src/components/ThemeToggle.tsx +0 -21
- package/app/src/hooks/useTheme.ts +0 -17
- package/app/src/layouts/dashboard.tsx +0 -259
- package/app/src/main.tsx +0 -121
- package/app/src/pages/dashboard-bots.tsx +0 -198
- package/app/src/pages/dashboard-home.tsx +0 -301
- package/app/src/pages/dashboard-logs.tsx +0 -298
- package/app/src/pages/dashboard-plugin-detail.tsx +0 -360
- package/app/src/pages/dashboard-plugins.tsx +0 -166
- package/app/src/style.css +0 -1105
- package/app/src/theme/index.ts +0 -92
- package/app/tailwind.config.js +0 -70
- package/app/tsconfig.json +0 -16
- /package/{src → client}/router/index.tsx +0 -0
- /package/{src → client}/store/index.ts +0 -0
- /package/{src → client}/store/reducers/config.ts +0 -0
- /package/{src → client}/store/reducers/index.ts +0 -0
- /package/{src → client}/types.ts +0 -0
- /package/{src → client}/websocket/hooks.ts +0 -0
- /package/{src → client}/websocket/index.ts +0 -0
- /package/{src → client}/websocket/instance.ts +0 -0
- /package/{src → client}/websocket/manager.ts +0 -0
- /package/{src → client}/websocket/messageHandler.ts +0 -0
- /package/{src → client}/websocket/types.ts +0 -0
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PluginConfigForm 主组件
|
|
3
|
-
* 插件配置表单 - 基于 Schema 自动生成
|
|
4
|
-
* 改为折叠面板形式,使用 WebSocket 传递配置
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useState, useEffect } from 'react'
|
|
8
|
-
import {
|
|
9
|
-
Flex,
|
|
10
|
-
Box,
|
|
11
|
-
Text,
|
|
12
|
-
Button,
|
|
13
|
-
Spinner,
|
|
14
|
-
Callout,
|
|
15
|
-
Separator,
|
|
16
|
-
Badge,
|
|
17
|
-
ScrollArea,
|
|
18
|
-
Card
|
|
19
|
-
} from '@radix-ui/themes'
|
|
20
|
-
import { Accordion } from 'radix-ui'
|
|
21
|
-
import { Icons, useConfig } from '@zhin.js/client'
|
|
22
|
-
import type { PluginConfigFormProps, SchemaField, Schema } from './types.js'
|
|
23
|
-
import { FieldRenderer, isComplexField } from './FieldRenderer.js'
|
|
24
|
-
import { NestedFieldRenderer } from './NestedFieldRenderer.js'
|
|
25
|
-
|
|
26
|
-
export function PluginConfigForm({ pluginName, onSuccess }: Omit<PluginConfigFormProps, 'schema' | 'initialConfig'>) {
|
|
27
|
-
const [localConfig, setLocalConfig] = useState<Record<string, any>>({})
|
|
28
|
-
const [successMessage, setSuccessMessage] = useState<string | null>(null)
|
|
29
|
-
const [isExpanded, setIsExpanded] = useState<string | undefined>(undefined)
|
|
30
|
-
|
|
31
|
-
// 使用 WebSocket 配置管理
|
|
32
|
-
const { config, schema, loading, error, connected, setConfig } = useConfig(pluginName)
|
|
33
|
-
// 当远程配置改变时更新本地配置
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (config) {
|
|
36
|
-
setLocalConfig(config)
|
|
37
|
-
}
|
|
38
|
-
}, [config])
|
|
39
|
-
|
|
40
|
-
const handleSave = async () => {
|
|
41
|
-
if (!connected) {
|
|
42
|
-
setSuccessMessage(null)
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
await setConfig(localConfig)
|
|
48
|
-
setSuccessMessage('配置已保存成功')
|
|
49
|
-
setTimeout(() => {
|
|
50
|
-
setIsExpanded(undefined) // 收起面板
|
|
51
|
-
onSuccess?.()
|
|
52
|
-
setSuccessMessage(null)
|
|
53
|
-
}, 1500)
|
|
54
|
-
} catch (err) {
|
|
55
|
-
console.error('保存配置失败:', err)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const handleFieldChange = (fieldName: string, value: any) => {
|
|
60
|
-
setLocalConfig(prev => ({ ...prev, [fieldName]: value }))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const handleNestedFieldChange = (parentPath: string, childKey: string, value: any) => {
|
|
64
|
-
setLocalConfig(prev => ({
|
|
65
|
-
...prev,
|
|
66
|
-
[parentPath]: {
|
|
67
|
-
...(prev[parentPath] || {}),
|
|
68
|
-
[childKey]: value
|
|
69
|
-
}
|
|
70
|
-
}))
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const handleArrayItemChange = (fieldName: string, index: number, value: any) => {
|
|
74
|
-
setLocalConfig(prev => {
|
|
75
|
-
const arr = Array.isArray(prev[fieldName]) ? [...prev[fieldName]] : []
|
|
76
|
-
arr[index] = value
|
|
77
|
-
return { ...prev, [fieldName]: arr }
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const renderField = (fieldName: string, field: SchemaField, parentPath?: string): React.ReactElement => {
|
|
82
|
-
const fullPath = parentPath ? `${parentPath}.${fieldName}` : fieldName
|
|
83
|
-
const value = parentPath
|
|
84
|
-
? localConfig[parentPath]?.[fieldName] ?? field.default
|
|
85
|
-
: localConfig[fieldName] ?? field.default
|
|
86
|
-
|
|
87
|
-
const onChange = parentPath
|
|
88
|
-
? (val: any) => handleNestedFieldChange(parentPath, fieldName, val)
|
|
89
|
-
: (val: any) => handleFieldChange(fieldName, val)
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<FieldRenderer
|
|
93
|
-
fieldName={fieldName}
|
|
94
|
-
field={field}
|
|
95
|
-
value={value}
|
|
96
|
-
onChange={onChange}
|
|
97
|
-
parentPath={parentPath}
|
|
98
|
-
onNestedChange={handleNestedFieldChange}
|
|
99
|
-
onArrayItemChange={handleArrayItemChange}
|
|
100
|
-
renderField={renderField}
|
|
101
|
-
renderNestedField={(fn, f, v, oc) => (
|
|
102
|
-
<NestedFieldRenderer fieldName={fn} field={f} value={v} onChange={oc} />
|
|
103
|
-
)}
|
|
104
|
-
/>
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const fields = schema?.properties || schema?.dict || {}
|
|
109
|
-
|
|
110
|
-
if (!schema || !fields || Object.keys(fields).length === 0) {
|
|
111
|
-
return null // 没有配置则不显示
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<Card size="2" className="mt-4">
|
|
116
|
-
<Accordion.Root
|
|
117
|
-
type="single"
|
|
118
|
-
collapsible
|
|
119
|
-
value={isExpanded}
|
|
120
|
-
onValueChange={setIsExpanded}
|
|
121
|
-
>
|
|
122
|
-
<Accordion.Item value="config" className="border-none">
|
|
123
|
-
<Accordion.Header>
|
|
124
|
-
<Accordion.Trigger className="w-full group">
|
|
125
|
-
<Flex
|
|
126
|
-
justify="between"
|
|
127
|
-
align="center"
|
|
128
|
-
className="w-full p-3 hover:bg-gray-50 dark:hover:bg-gray-900/50 rounded-lg transition-colors"
|
|
129
|
-
>
|
|
130
|
-
<Flex align="center" gap="2">
|
|
131
|
-
<Icons.Settings className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
|
132
|
-
<Text size="3" weight="bold">插件配置</Text>
|
|
133
|
-
<Badge size="1" color="gray" variant="soft">
|
|
134
|
-
{Object.keys(fields).length} 项
|
|
135
|
-
</Badge>
|
|
136
|
-
</Flex>
|
|
137
|
-
<Icons.ChevronDown
|
|
138
|
-
className="w-5 h-5 text-gray-500 transition-transform duration-200 group-data-[state=open]:rotate-180"
|
|
139
|
-
/>
|
|
140
|
-
</Flex>
|
|
141
|
-
</Accordion.Trigger>
|
|
142
|
-
</Accordion.Header>
|
|
143
|
-
|
|
144
|
-
<Accordion.Content className="overflow-hidden data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up">
|
|
145
|
-
<Box className="pt-2 pb-4">
|
|
146
|
-
{/* 成功提示 */}
|
|
147
|
-
{successMessage && (
|
|
148
|
-
<div className="mb-3 animate-in fade-in slide-in-from-top-2 duration-300">
|
|
149
|
-
<Callout.Root color="green" size="1" className="shadow-sm">
|
|
150
|
-
<Callout.Icon><Icons.CheckCircle /></Callout.Icon>
|
|
151
|
-
<Callout.Text className="font-medium">{successMessage}</Callout.Text>
|
|
152
|
-
</Callout.Root>
|
|
153
|
-
</div>
|
|
154
|
-
)}
|
|
155
|
-
|
|
156
|
-
{/* 错误提示 */}
|
|
157
|
-
{error && (
|
|
158
|
-
<div className="mb-3 animate-in fade-in slide-in-from-top-2 duration-300">
|
|
159
|
-
<Callout.Root color="red" size="1" className="shadow-sm">
|
|
160
|
-
<Callout.Icon><Icons.AlertCircle /></Callout.Icon>
|
|
161
|
-
<Callout.Text className="font-medium">{error}</Callout.Text>
|
|
162
|
-
</Callout.Root>
|
|
163
|
-
</div>
|
|
164
|
-
)}
|
|
165
|
-
|
|
166
|
-
{/* 配置表单 */}
|
|
167
|
-
<Flex direction="column" gap="3">
|
|
168
|
-
{Object.entries(fields).map(([fieldName, field]) => {
|
|
169
|
-
const schemaField = field as SchemaField
|
|
170
|
-
return (
|
|
171
|
-
<div
|
|
172
|
-
key={fieldName}
|
|
173
|
-
className="group p-3 rounded-lg bg-gray-50 dark:bg-gray-900/50 hover:bg-gray-100 dark:hover:bg-gray-900 transition-colors border border-gray-200 dark:border-gray-800"
|
|
174
|
-
>
|
|
175
|
-
<Flex direction="column" gap="2">
|
|
176
|
-
<Flex align="center" gap="1">
|
|
177
|
-
<Text size="2" weight="bold" className="text-gray-900 dark:text-gray-100">
|
|
178
|
-
{schemaField.key || fieldName}
|
|
179
|
-
</Text>
|
|
180
|
-
{schemaField.required && (
|
|
181
|
-
<Text size="2" weight="bold" color="red" className="leading-none">
|
|
182
|
-
*
|
|
183
|
-
</Text>
|
|
184
|
-
)}
|
|
185
|
-
</Flex>
|
|
186
|
-
{schemaField.description && (
|
|
187
|
-
<Text size="1" color="gray" className="leading-relaxed">
|
|
188
|
-
{schemaField.description}
|
|
189
|
-
</Text>
|
|
190
|
-
)}
|
|
191
|
-
<div className="mt-1">
|
|
192
|
-
{renderField(schemaField.key || fieldName, schemaField)}
|
|
193
|
-
</div>
|
|
194
|
-
</Flex>
|
|
195
|
-
</div>
|
|
196
|
-
)
|
|
197
|
-
})}
|
|
198
|
-
</Flex>
|
|
199
|
-
|
|
200
|
-
{/* 操作按钮 */}
|
|
201
|
-
<Flex gap="2" justify="end" className="mt-4 pt-3 border-t border-gray-200 dark:border-gray-800">
|
|
202
|
-
<Button
|
|
203
|
-
size="2"
|
|
204
|
-
variant="soft"
|
|
205
|
-
onClick={() => setIsExpanded(undefined)}
|
|
206
|
-
disabled={loading}
|
|
207
|
-
className="hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors"
|
|
208
|
-
>
|
|
209
|
-
<Icons.X className="w-4 h-4" />
|
|
210
|
-
取消
|
|
211
|
-
</Button>
|
|
212
|
-
<Button
|
|
213
|
-
size="2"
|
|
214
|
-
onClick={handleSave}
|
|
215
|
-
disabled={loading}
|
|
216
|
-
className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white transition-colors shadow-sm"
|
|
217
|
-
>
|
|
218
|
-
{loading ? (
|
|
219
|
-
<>
|
|
220
|
-
<Spinner />
|
|
221
|
-
<span>保存中...</span>
|
|
222
|
-
</>
|
|
223
|
-
) : (
|
|
224
|
-
<>
|
|
225
|
-
<Icons.Save className="w-4 h-4" />
|
|
226
|
-
<span>保存配置</span>
|
|
227
|
-
</>
|
|
228
|
-
)}
|
|
229
|
-
</Button>
|
|
230
|
-
</Flex>
|
|
231
|
-
</Box>
|
|
232
|
-
</Accordion.Content>
|
|
233
|
-
</Accordion.Item>
|
|
234
|
-
</Accordion.Root>
|
|
235
|
-
</Card>
|
|
236
|
-
)
|
|
237
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PluginConfigForm 类型定义
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface SchemaField {
|
|
6
|
-
key?: string
|
|
7
|
-
type: string
|
|
8
|
-
description?: string
|
|
9
|
-
default?: any
|
|
10
|
-
required?: boolean
|
|
11
|
-
enum?: any[]
|
|
12
|
-
min?: number
|
|
13
|
-
max?: number
|
|
14
|
-
step?: number // 用于 percent 类型
|
|
15
|
-
pattern?: string
|
|
16
|
-
inner?: SchemaField // 用于 list/dict 类型
|
|
17
|
-
items?: SchemaField // 兼容旧格式
|
|
18
|
-
list?: SchemaField[] // 用于 tuple/union/intersect 类型
|
|
19
|
-
properties?: Record<string, SchemaField>
|
|
20
|
-
dict?: Record<string, SchemaField> // object 类型的字段
|
|
21
|
-
component?: string // UI 组件提示
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface Schema {
|
|
25
|
-
type: string
|
|
26
|
-
properties?: Record<string, SchemaField>
|
|
27
|
-
dict?: Record<string, SchemaField>
|
|
28
|
-
description?: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface PluginConfigFormProps {
|
|
32
|
-
pluginName: string
|
|
33
|
-
schema: Schema | null
|
|
34
|
-
initialConfig?: Record<string, any>
|
|
35
|
-
onSuccess?: () => void
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface FieldRendererProps {
|
|
39
|
-
fieldName: string
|
|
40
|
-
field: SchemaField
|
|
41
|
-
value: any
|
|
42
|
-
onChange: (value: any) => void
|
|
43
|
-
parentPath?: string
|
|
44
|
-
onNestedChange?: (parentPath: string, childKey: string, value: any) => void
|
|
45
|
-
onArrayItemChange?: (fieldName: string, index: number, value: any) => void
|
|
46
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { useTheme } from '../hooks/useTheme'
|
|
2
|
-
import { Icons } from '@zhin.js/client'
|
|
3
|
-
|
|
4
|
-
export function ThemeToggle() {
|
|
5
|
-
const { theme, toggleTheme } = useTheme()
|
|
6
|
-
|
|
7
|
-
return (
|
|
8
|
-
<button
|
|
9
|
-
onClick={toggleTheme}
|
|
10
|
-
className="p-2 hover:bg-accent rounded-lg transition-colors text-foreground"
|
|
11
|
-
title={theme === 'light' ? '切换到暗色模式' : '切换到亮色模式'}
|
|
12
|
-
>
|
|
13
|
-
{theme === 'light' ? (
|
|
14
|
-
<Icons.Moon className="w-5 h-5" />
|
|
15
|
-
) : (
|
|
16
|
-
<Icons.Sun className="w-5 h-5" />
|
|
17
|
-
)}
|
|
18
|
-
</button>
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
2
|
-
import { type Theme, applyTheme, getInitialTheme } from '../theme'
|
|
3
|
-
|
|
4
|
-
export function useTheme() {
|
|
5
|
-
const [theme, setTheme] = useState<Theme>(getInitialTheme)
|
|
6
|
-
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
applyTheme(theme)
|
|
9
|
-
}, [theme])
|
|
10
|
-
|
|
11
|
-
const toggleTheme = () => {
|
|
12
|
-
setTheme(prev => prev === 'light' ? 'dark' : 'light')
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return { theme, setTheme, toggleTheme }
|
|
16
|
-
}
|
|
17
|
-
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import { Outlet, Link, useSelector, useDispatch, toggleSidebar, setActiveMenu, useWebSocket } from "@zhin.js/client"
|
|
2
|
-
import React, { useMemo } from "react"
|
|
3
|
-
import { Avatar, DropdownMenu } from 'radix-ui'
|
|
4
|
-
import * as Themes from '@radix-ui/themes'
|
|
5
|
-
import { Icons, cn } from "@zhin.js/client"
|
|
6
|
-
import { ThemeToggle } from "../components/ThemeToggle"
|
|
7
|
-
|
|
8
|
-
const { Box, Flex, Text, Heading, IconButton, Badge, TextField, ScrollArea, Container } = Themes
|
|
9
|
-
|
|
10
|
-
export default function DashboardLayout() {
|
|
11
|
-
const dispatch = useDispatch()
|
|
12
|
-
const ws = useWebSocket()
|
|
13
|
-
const sidebarOpen = useSelector(state => state.ui.sidebarOpen)
|
|
14
|
-
const activeMenu = useSelector(state => state.ui.activeMenu)
|
|
15
|
-
const routes = useSelector(state => state.route.routes)
|
|
16
|
-
|
|
17
|
-
const menuItems = useMemo(() => {
|
|
18
|
-
// 找到 dashboard 路由
|
|
19
|
-
const dashboardRoute = routes.find(route => route.key === 'dashboard-layout')
|
|
20
|
-
if (!dashboardRoute || !dashboardRoute.children) {
|
|
21
|
-
return []
|
|
22
|
-
}
|
|
23
|
-
return dashboardRoute.children
|
|
24
|
-
.filter(route => !route.meta?.hideInMenu && route.key !== 'dashboard-layout')
|
|
25
|
-
.map((route, index) => ({
|
|
26
|
-
key: route.key || `menu-item-${index}`,
|
|
27
|
-
title: route.title,
|
|
28
|
-
index: route.index,
|
|
29
|
-
icon: route.icon,
|
|
30
|
-
href: route.path
|
|
31
|
-
}))
|
|
32
|
-
}, [routes])
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<Flex className="h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
|
36
|
-
{/* 侧边栏 */}
|
|
37
|
-
<Flex
|
|
38
|
-
direction="column"
|
|
39
|
-
className={cn(
|
|
40
|
-
"glass transition-all duration-300 shadow-xl border-r border-gray-200/50 dark:border-gray-700/50",
|
|
41
|
-
sidebarOpen ? "w-64" : "w-20"
|
|
42
|
-
)}
|
|
43
|
-
>
|
|
44
|
-
{/* Logo 区域 */}
|
|
45
|
-
<Box p="4" className="border-b border-gray-200/50 dark:border-gray-700/50">
|
|
46
|
-
<Flex
|
|
47
|
-
align="center"
|
|
48
|
-
className={cn(
|
|
49
|
-
"transition-all duration-300",
|
|
50
|
-
sidebarOpen ? "gap-3" : "justify-center"
|
|
51
|
-
)}
|
|
52
|
-
>
|
|
53
|
-
<Flex
|
|
54
|
-
align="center"
|
|
55
|
-
justify="center"
|
|
56
|
-
className="w-11 min-w-11 h-11 rounded-xl shadow-md bg-gradient-to-br from-blue-500 to-purple-600"
|
|
57
|
-
>
|
|
58
|
-
<Text size="5" weight="bold" className="text-white">
|
|
59
|
-
Z
|
|
60
|
-
</Text>
|
|
61
|
-
</Flex>
|
|
62
|
-
{sidebarOpen && (
|
|
63
|
-
<Flex direction="column" gap="0">
|
|
64
|
-
<Heading size="4" className="text-gray-900 dark:text-gray-100">
|
|
65
|
-
Zhin.js
|
|
66
|
-
</Heading>
|
|
67
|
-
<Text size="1" color="gray">管理控制台</Text>
|
|
68
|
-
</Flex>
|
|
69
|
-
)}
|
|
70
|
-
</Flex>
|
|
71
|
-
</Box>
|
|
72
|
-
|
|
73
|
-
{/* 菜单列表 */}
|
|
74
|
-
<ScrollArea className="flex-1" scrollbars="vertical">
|
|
75
|
-
<Box p="3">
|
|
76
|
-
<Flex direction="column" gap="2">
|
|
77
|
-
{menuItems.map((item) => {
|
|
78
|
-
const isActive = activeMenu === item.key
|
|
79
|
-
return (
|
|
80
|
-
<Link
|
|
81
|
-
key={item.key}
|
|
82
|
-
to={item.href}
|
|
83
|
-
onClick={() => dispatch(setActiveMenu(item.key))}
|
|
84
|
-
className={cn("menu-item", isActive && "active")}
|
|
85
|
-
>
|
|
86
|
-
{item.icon && React.isValidElement(item.icon) && (
|
|
87
|
-
<div className="icon">
|
|
88
|
-
{item.icon}
|
|
89
|
-
</div>
|
|
90
|
-
)}
|
|
91
|
-
{sidebarOpen && (
|
|
92
|
-
<div className="text">
|
|
93
|
-
<div className="title">{item.title}</div>
|
|
94
|
-
</div>
|
|
95
|
-
)}
|
|
96
|
-
{isActive && sidebarOpen && <div className="indicator" />}
|
|
97
|
-
</Link>
|
|
98
|
-
)
|
|
99
|
-
})}
|
|
100
|
-
</Flex>
|
|
101
|
-
</Box>
|
|
102
|
-
</ScrollArea>
|
|
103
|
-
|
|
104
|
-
{/* 侧边栏底部用户信息 */}
|
|
105
|
-
<Flex p="3" className="border-t border-gray-200/50 dark:border-gray-700/50 flex-shrink-0">
|
|
106
|
-
<Flex
|
|
107
|
-
align="center"
|
|
108
|
-
gap="2"
|
|
109
|
-
p="2"
|
|
110
|
-
className={cn(
|
|
111
|
-
"rounded-xl bg-gray-50/50 dark:bg-gray-800/50 transition-all duration-200 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700",
|
|
112
|
-
!sidebarOpen && "justify-center"
|
|
113
|
-
)}
|
|
114
|
-
>
|
|
115
|
-
<Avatar.Root className="w-8 h-8 min-w-8 border-2 border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
116
|
-
<Avatar.Image src="https://i.pravatar.cc/150?u=admin" alt="管理员" />
|
|
117
|
-
<Avatar.Fallback>管</Avatar.Fallback>
|
|
118
|
-
</Avatar.Root>
|
|
119
|
-
{sidebarOpen && (
|
|
120
|
-
<Flex direction="column" gap="0" className="flex-1 min-w-0">
|
|
121
|
-
<Text size="2" weight="medium" className="truncate text-gray-900 dark:text-gray-100">
|
|
122
|
-
管理员
|
|
123
|
-
</Text>
|
|
124
|
-
<Text size="1" color="gray" className="truncate">
|
|
125
|
-
admin@zhin.com
|
|
126
|
-
</Text>
|
|
127
|
-
</Flex>
|
|
128
|
-
)}
|
|
129
|
-
</Flex>
|
|
130
|
-
</Flex>
|
|
131
|
-
</Flex>
|
|
132
|
-
|
|
133
|
-
{/* 主内容区域 */}
|
|
134
|
-
<Flex direction="column" className="flex-1 overflow-hidden">
|
|
135
|
-
{/* 顶部导航栏 */}
|
|
136
|
-
<Box className="glass border-b border-gray-200/50 dark:border-gray-700/50 shadow-sm">
|
|
137
|
-
<Flex justify="between" align="center" px="4" className="h-16">
|
|
138
|
-
{/* 左侧 */}
|
|
139
|
-
<Flex align="center" gap="3">
|
|
140
|
-
<IconButton
|
|
141
|
-
variant="ghost"
|
|
142
|
-
size="2"
|
|
143
|
-
onClick={() => dispatch(toggleSidebar())}
|
|
144
|
-
className="hover-lift rounded-xl"
|
|
145
|
-
>
|
|
146
|
-
<Icons.Menu className="w-5 h-5" />
|
|
147
|
-
</IconButton>
|
|
148
|
-
<Flex direction="column" gap="0">
|
|
149
|
-
<Heading size="3" className="text-gray-900 dark:text-gray-100">控制台</Heading>
|
|
150
|
-
<Text size="1" color="gray">欢迎回来!</Text>
|
|
151
|
-
</Flex>
|
|
152
|
-
</Flex>
|
|
153
|
-
|
|
154
|
-
{/* 中间搜索栏 */}
|
|
155
|
-
<Flex className="hidden md:flex flex-1 max-w-xl mx-6">
|
|
156
|
-
<Box className="relative">
|
|
157
|
-
<Icons.Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 dark:text-gray-500" />
|
|
158
|
-
<TextField.Root
|
|
159
|
-
placeholder="搜索功能、用户、设置..."
|
|
160
|
-
size="2"
|
|
161
|
-
className="w-full pl-10"
|
|
162
|
-
/>
|
|
163
|
-
</Box>
|
|
164
|
-
</Flex>
|
|
165
|
-
|
|
166
|
-
{/* 右侧操作区 */}
|
|
167
|
-
<Flex align="center" gap="2">
|
|
168
|
-
{/* 主题切换 */}
|
|
169
|
-
<ThemeToggle />
|
|
170
|
-
|
|
171
|
-
{/* 通知按钮 */}
|
|
172
|
-
<Box className="relative">
|
|
173
|
-
<IconButton
|
|
174
|
-
variant="ghost"
|
|
175
|
-
size="2"
|
|
176
|
-
className="hover-lift rounded-xl"
|
|
177
|
-
>
|
|
178
|
-
<Icons.Bell className="w-5 h-5" />
|
|
179
|
-
</IconButton>
|
|
180
|
-
<Badge
|
|
181
|
-
color="red"
|
|
182
|
-
variant="solid"
|
|
183
|
-
size="1"
|
|
184
|
-
className="absolute -top-1 -right-1 min-w-5 h-5 flex items-center justify-center p-0"
|
|
185
|
-
>
|
|
186
|
-
3
|
|
187
|
-
</Badge>
|
|
188
|
-
</Box>
|
|
189
|
-
|
|
190
|
-
{/* 用户菜单 */}
|
|
191
|
-
<DropdownMenu.Root>
|
|
192
|
-
<DropdownMenu.Trigger asChild>
|
|
193
|
-
<Flex
|
|
194
|
-
align="center"
|
|
195
|
-
gap="2"
|
|
196
|
-
px="2"
|
|
197
|
-
py="1"
|
|
198
|
-
className="cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-all duration-200 rounded-xl"
|
|
199
|
-
>
|
|
200
|
-
<Avatar.Root className="w-8 h-8 border-2 border-blue-500/20 dark:border-blue-400/30">
|
|
201
|
-
<Avatar.Image src="https://i.pravatar.cc/150?u=admin" alt="管理员" />
|
|
202
|
-
<Avatar.Fallback>管</Avatar.Fallback>
|
|
203
|
-
</Avatar.Root>
|
|
204
|
-
<Flex direction="column" gap="0" className="hidden lg:flex">
|
|
205
|
-
<Text size="2" weight="medium" className="text-gray-900 dark:text-gray-100">
|
|
206
|
-
管理员
|
|
207
|
-
</Text>
|
|
208
|
-
<Text size="1" color="gray">
|
|
209
|
-
admin@zhin.com
|
|
210
|
-
</Text>
|
|
211
|
-
</Flex>
|
|
212
|
-
</Flex>
|
|
213
|
-
</DropdownMenu.Trigger>
|
|
214
|
-
<DropdownMenu.Content align="end" className="min-w-56">
|
|
215
|
-
<Box p="3" className="border-b border-gray-200 dark:border-gray-700">
|
|
216
|
-
<Text size="2" weight="bold" className="block">
|
|
217
|
-
登录为
|
|
218
|
-
</Text>
|
|
219
|
-
<Text size="1" color="gray" className="block">
|
|
220
|
-
admin@zhin.com
|
|
221
|
-
</Text>
|
|
222
|
-
</Box>
|
|
223
|
-
<DropdownMenu.Item>
|
|
224
|
-
<Icons.User className="mr-2 h-4 w-4" />
|
|
225
|
-
我的设置
|
|
226
|
-
</DropdownMenu.Item>
|
|
227
|
-
<DropdownMenu.Item>
|
|
228
|
-
<Icons.Users className="mr-2 h-4 w-4" />
|
|
229
|
-
团队设置
|
|
230
|
-
</DropdownMenu.Item>
|
|
231
|
-
<DropdownMenu.Item>
|
|
232
|
-
<Icons.BarChart3 className="mr-2 h-4 w-4" />
|
|
233
|
-
数据分析
|
|
234
|
-
</DropdownMenu.Item>
|
|
235
|
-
<DropdownMenu.Item>
|
|
236
|
-
<Icons.HelpCircle className="mr-2 h-4 w-4" />
|
|
237
|
-
帮助与反馈
|
|
238
|
-
</DropdownMenu.Item>
|
|
239
|
-
<DropdownMenu.Separator />
|
|
240
|
-
<DropdownMenu.Item color="red">
|
|
241
|
-
<Icons.LogOut className="mr-2 h-4 w-4" />
|
|
242
|
-
退出登录
|
|
243
|
-
</DropdownMenu.Item>
|
|
244
|
-
</DropdownMenu.Content>
|
|
245
|
-
</DropdownMenu.Root>
|
|
246
|
-
</Flex>
|
|
247
|
-
</Flex>
|
|
248
|
-
</Box>
|
|
249
|
-
|
|
250
|
-
{/* 主内容区域 */}
|
|
251
|
-
<Flex className="flex-1 overflow-auto">
|
|
252
|
-
<Container size="4" p="6">
|
|
253
|
-
<Outlet />
|
|
254
|
-
</Container>
|
|
255
|
-
</Flex>
|
|
256
|
-
</Flex>
|
|
257
|
-
</Flex>
|
|
258
|
-
)
|
|
259
|
-
}
|