@zhin.js/console 1.0.40 → 1.0.42
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/CHANGELOG.md +19 -0
- package/client/src/components/PluginConfigForm/index.tsx +20 -5
- package/client/src/components/PluginConfigForm/types.ts +1 -0
- package/client/src/main.tsx +21 -3
- package/client/src/pages/dashboard-config.tsx +421 -0
- package/client/src/pages/dashboard-env.tsx +219 -0
- package/dist/client.js +1 -1
- package/dist/index.js +137 -6
- package/dist/style.css +2 -2
- package/lib/index.js +204 -81
- package/lib/websocket.d.ts +0 -9
- package/lib/websocket.js +174 -56
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @zhin.js/console
|
|
2
2
|
|
|
3
|
+
## 1.0.42
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- zhin.js@1.0.44
|
|
8
|
+
- @zhin.js/http@1.0.38
|
|
9
|
+
- @zhin.js/core@1.0.44
|
|
10
|
+
|
|
11
|
+
## 1.0.41
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 72ec4ba: fix: 新增插件,控制台调优
|
|
16
|
+
- Updated dependencies [72ec4ba]
|
|
17
|
+
- @zhin.js/http@1.0.37
|
|
18
|
+
- @zhin.js/client@1.0.11
|
|
19
|
+
- @zhin.js/core@1.0.43
|
|
20
|
+
- zhin.js@1.0.43
|
|
21
|
+
|
|
3
22
|
## 1.0.40
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { useState, useEffect } from 'react'
|
|
6
6
|
import { useConfig } from '@zhin.js/client'
|
|
7
7
|
import type { PluginConfigFormProps, SchemaField } from './types.js'
|
|
8
|
-
import { Settings, ChevronDown, CheckCircle, AlertCircle, X, Save, Loader2 } from 'lucide-react'
|
|
8
|
+
import { Settings, ChevronDown, CheckCircle, AlertCircle, AlertTriangle, X, Save, Loader2 } from 'lucide-react'
|
|
9
9
|
import { FieldRenderer, isComplexField } from './FieldRenderer.js'
|
|
10
10
|
import { NestedFieldRenderer } from './NestedFieldRenderer.js'
|
|
11
11
|
import { Card } from '../ui/card'
|
|
@@ -25,12 +25,21 @@ export function PluginConfigForm({ pluginName, onSuccess }: Omit<PluginConfigFor
|
|
|
25
25
|
if (config) setLocalConfig(config)
|
|
26
26
|
}, [config])
|
|
27
27
|
|
|
28
|
+
const [warnMessage, setWarnMessage] = useState<string | null>(null)
|
|
29
|
+
|
|
28
30
|
const handleSave = async () => {
|
|
29
31
|
if (!connected) return
|
|
30
32
|
try {
|
|
31
|
-
await setConfig(localConfig)
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const result = await setConfig(localConfig)
|
|
34
|
+
if (result?.reloaded) {
|
|
35
|
+
setSuccessMessage('配置已保存,插件已重载')
|
|
36
|
+
} else if (result?.message) {
|
|
37
|
+
setWarnMessage(result.message)
|
|
38
|
+
setSuccessMessage(null)
|
|
39
|
+
} else {
|
|
40
|
+
setSuccessMessage('配置已保存')
|
|
41
|
+
}
|
|
42
|
+
setTimeout(() => { setIsExpanded(undefined); onSuccess?.(); setSuccessMessage(null); setWarnMessage(null) }, 2500)
|
|
34
43
|
} catch (err) {
|
|
35
44
|
console.error('保存配置失败:', err)
|
|
36
45
|
}
|
|
@@ -74,7 +83,7 @@ export function PluginConfigForm({ pluginName, onSuccess }: Omit<PluginConfigFor
|
|
|
74
83
|
)
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
const fields = schema?.properties || schema?.dict || {}
|
|
86
|
+
const fields = schema?.object || schema?.properties || schema?.dict || {}
|
|
78
87
|
if (!schema || !fields || Object.keys(fields).length === 0) return null
|
|
79
88
|
|
|
80
89
|
return (
|
|
@@ -95,6 +104,12 @@ export function PluginConfigForm({ pluginName, onSuccess }: Omit<PluginConfigFor
|
|
|
95
104
|
<AlertDescription>{successMessage}</AlertDescription>
|
|
96
105
|
</Alert>
|
|
97
106
|
)}
|
|
107
|
+
{warnMessage && !successMessage && (
|
|
108
|
+
<Alert className="mb-3 border-yellow-500/50 text-yellow-700 dark:text-yellow-400">
|
|
109
|
+
<AlertTriangle className="h-4 w-4" />
|
|
110
|
+
<AlertDescription>{warnMessage}</AlertDescription>
|
|
111
|
+
</Alert>
|
|
112
|
+
)}
|
|
98
113
|
{error && (
|
|
99
114
|
<Alert variant="destructive" className="mb-3">
|
|
100
115
|
<AlertCircle className="h-4 w-4" />
|
package/client/src/main.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StrictMode, useCallback, useEffect, useState } from 'react'
|
|
2
2
|
import { createRoot } from 'react-dom/client'
|
|
3
3
|
import { Provider as ReduxProvider } from 'react-redux'
|
|
4
|
-
import { Home, Package, Bot, FileText } from 'lucide-react'
|
|
4
|
+
import { Home, Package, Bot, FileText, Settings, KeyRound } from 'lucide-react'
|
|
5
5
|
import { store, DynamicRouter, persistor, addPage, useSelector, useWebSocket } from '@zhin.js/client'
|
|
6
6
|
import DashboardLayout from './layouts/dashboard'
|
|
7
7
|
import DashboardHome from './pages/dashboard-home'
|
|
@@ -9,6 +9,8 @@ import DashboardPlugins from './pages/dashboard-plugins'
|
|
|
9
9
|
import DashboardPluginDetail from './pages/dashboard-plugin-detail'
|
|
10
10
|
import DashboardBots from './pages/dashboard-bots'
|
|
11
11
|
import DashboardLogs from './pages/dashboard-logs'
|
|
12
|
+
import DashboardConfig from './pages/dashboard-config'
|
|
13
|
+
import DashboardEnv from './pages/dashboard-env'
|
|
12
14
|
import LoginPage from './pages/login'
|
|
13
15
|
import { hasToken } from './utils/auth'
|
|
14
16
|
import './style.css'
|
|
@@ -83,13 +85,29 @@ function RouteInitializer() {
|
|
|
83
85
|
element: <DashboardPluginDetail />,
|
|
84
86
|
meta: { hideInMenu: true }
|
|
85
87
|
},
|
|
88
|
+
{
|
|
89
|
+
key: 'dashboard-config',
|
|
90
|
+
path: '/config',
|
|
91
|
+
title: '配置管理',
|
|
92
|
+
icon: <Settings className="w-4 h-4" />,
|
|
93
|
+
element: <DashboardConfig />,
|
|
94
|
+
meta: { order: 3 }
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
key: 'dashboard-env',
|
|
98
|
+
path: '/env',
|
|
99
|
+
title: '环境变量',
|
|
100
|
+
icon: <KeyRound className="w-4 h-4" />,
|
|
101
|
+
element: <DashboardEnv />,
|
|
102
|
+
meta: { order: 4 }
|
|
103
|
+
},
|
|
86
104
|
{
|
|
87
105
|
key: 'dashboard-bots',
|
|
88
106
|
path: '/bots',
|
|
89
107
|
title: '机器人',
|
|
90
108
|
icon: <Bot className="w-4 h-4" />,
|
|
91
109
|
element: <DashboardBots />,
|
|
92
|
-
meta: { order:
|
|
110
|
+
meta: { order: 5 }
|
|
93
111
|
},
|
|
94
112
|
{
|
|
95
113
|
key: 'dashboard-logs',
|
|
@@ -97,7 +115,7 @@ function RouteInitializer() {
|
|
|
97
115
|
title: '系统日志',
|
|
98
116
|
icon: <FileText className="w-4 h-4" />,
|
|
99
117
|
element: <DashboardLogs />,
|
|
100
|
-
meta: { order:
|
|
118
|
+
meta: { order: 6 }
|
|
101
119
|
}
|
|
102
120
|
]
|
|
103
121
|
}
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
+
import { useConfigYaml } from '@zhin.js/client'
|
|
3
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
|
|
4
|
+
import {
|
|
5
|
+
Settings, AlertCircle, CheckCircle, Save, Loader2, X,
|
|
6
|
+
RefreshCw, FileCode, FormInput
|
|
7
|
+
} from 'lucide-react'
|
|
8
|
+
import { Card, CardContent } from '../components/ui/card'
|
|
9
|
+
import { Badge } from '../components/ui/badge'
|
|
10
|
+
import { Button } from '../components/ui/button'
|
|
11
|
+
import { Alert, AlertDescription } from '../components/ui/alert'
|
|
12
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../components/ui/tabs'
|
|
13
|
+
import { Textarea } from '../components/ui/textarea'
|
|
14
|
+
import { Input } from '../components/ui/input'
|
|
15
|
+
import { Separator } from '../components/ui/separator'
|
|
16
|
+
|
|
17
|
+
function GeneralConfigForm({
|
|
18
|
+
config,
|
|
19
|
+
pluginKeys,
|
|
20
|
+
onSave,
|
|
21
|
+
saving
|
|
22
|
+
}: {
|
|
23
|
+
config: Record<string, any>
|
|
24
|
+
pluginKeys: string[]
|
|
25
|
+
onSave: (patch: Record<string, any>) => Promise<void>
|
|
26
|
+
saving: boolean
|
|
27
|
+
}) {
|
|
28
|
+
const generalKeys = useMemo(() => {
|
|
29
|
+
const excludeSet = new Set(pluginKeys)
|
|
30
|
+
excludeSet.add('plugins')
|
|
31
|
+
return Object.keys(config).filter(k => !excludeSet.has(k))
|
|
32
|
+
}, [config, pluginKeys])
|
|
33
|
+
|
|
34
|
+
const [localValues, setLocalValues] = useState<Record<string, any>>({})
|
|
35
|
+
const [dirty, setDirty] = useState(false)
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const vals: Record<string, any> = {}
|
|
39
|
+
for (const key of generalKeys) {
|
|
40
|
+
vals[key] = config[key]
|
|
41
|
+
}
|
|
42
|
+
setLocalValues(vals)
|
|
43
|
+
setDirty(false)
|
|
44
|
+
}, [config, generalKeys])
|
|
45
|
+
|
|
46
|
+
const handleChange = (key: string, value: any) => {
|
|
47
|
+
setLocalValues(prev => ({ ...prev, [key]: value }))
|
|
48
|
+
setDirty(true)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleSave = async () => {
|
|
52
|
+
await onSave(localValues)
|
|
53
|
+
setDirty(false)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const handleReset = () => {
|
|
57
|
+
const vals: Record<string, any> = {}
|
|
58
|
+
for (const key of generalKeys) {
|
|
59
|
+
vals[key] = config[key]
|
|
60
|
+
}
|
|
61
|
+
setLocalValues(vals)
|
|
62
|
+
setDirty(false)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (generalKeys.length === 0) {
|
|
66
|
+
return (
|
|
67
|
+
<Card>
|
|
68
|
+
<CardContent className="flex flex-col items-center gap-3 py-12">
|
|
69
|
+
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-muted">
|
|
70
|
+
<Settings className="w-8 h-8 text-muted-foreground" />
|
|
71
|
+
</div>
|
|
72
|
+
<h3 className="text-lg font-semibold">暂无通用配置</h3>
|
|
73
|
+
<p className="text-sm text-muted-foreground">配置文件中未发现可编辑的通用字段</p>
|
|
74
|
+
</CardContent>
|
|
75
|
+
</Card>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="space-y-4">
|
|
81
|
+
<div className="space-y-3">
|
|
82
|
+
{generalKeys.map(key => (
|
|
83
|
+
<ConfigFieldEditor
|
|
84
|
+
key={key}
|
|
85
|
+
fieldKey={key}
|
|
86
|
+
value={localValues[key]}
|
|
87
|
+
onChange={val => handleChange(key, val)}
|
|
88
|
+
/>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
<div className="flex items-center gap-2 pt-2">
|
|
92
|
+
<Button size="sm" onClick={handleSave} disabled={saving || !dirty}>
|
|
93
|
+
{saving
|
|
94
|
+
? <><Loader2 className="w-4 h-4 mr-1 animate-spin" />保存中...</>
|
|
95
|
+
: <><Save className="w-4 h-4 mr-1" />保存</>}
|
|
96
|
+
</Button>
|
|
97
|
+
{dirty && (
|
|
98
|
+
<Button variant="outline" size="sm" onClick={handleReset}>
|
|
99
|
+
<X className="w-4 h-4 mr-1" />撤销
|
|
100
|
+
</Button>
|
|
101
|
+
)}
|
|
102
|
+
{dirty && <span className="text-xs text-muted-foreground">有未保存的更改</span>}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ConfigFieldEditor({
|
|
109
|
+
fieldKey,
|
|
110
|
+
value,
|
|
111
|
+
onChange
|
|
112
|
+
}: {
|
|
113
|
+
fieldKey: string
|
|
114
|
+
value: any
|
|
115
|
+
onChange: (val: any) => void
|
|
116
|
+
}) {
|
|
117
|
+
const valueType = typeof value
|
|
118
|
+
|
|
119
|
+
if (value === null || value === undefined) {
|
|
120
|
+
return (
|
|
121
|
+
<div className="p-3 rounded-lg bg-muted/50 border space-y-1.5">
|
|
122
|
+
<div className="flex items-center gap-1.5">
|
|
123
|
+
<span className="text-sm font-medium">{fieldKey}</span>
|
|
124
|
+
<Badge variant="outline" className="text-[10px] px-1 py-0">null</Badge>
|
|
125
|
+
</div>
|
|
126
|
+
<Input
|
|
127
|
+
value=""
|
|
128
|
+
placeholder="(空值)"
|
|
129
|
+
onChange={e => onChange(e.target.value || null)}
|
|
130
|
+
className="h-8 text-sm"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (valueType === 'boolean') {
|
|
137
|
+
return (
|
|
138
|
+
<div className="p-3 rounded-lg bg-muted/50 border space-y-1.5">
|
|
139
|
+
<div className="flex items-center justify-between">
|
|
140
|
+
<div className="flex items-center gap-1.5">
|
|
141
|
+
<span className="text-sm font-medium">{fieldKey}</span>
|
|
142
|
+
<Badge variant="outline" className="text-[10px] px-1 py-0">boolean</Badge>
|
|
143
|
+
</div>
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
onClick={() => onChange(!value)}
|
|
147
|
+
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
|
148
|
+
value ? 'bg-primary' : 'bg-muted-foreground/30'
|
|
149
|
+
}`}
|
|
150
|
+
>
|
|
151
|
+
<span
|
|
152
|
+
className={`inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform ${
|
|
153
|
+
value ? 'translate-x-4.5' : 'translate-x-0.5'
|
|
154
|
+
}`}
|
|
155
|
+
/>
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (valueType === 'number') {
|
|
163
|
+
return (
|
|
164
|
+
<div className="p-3 rounded-lg bg-muted/50 border space-y-1.5">
|
|
165
|
+
<div className="flex items-center gap-1.5">
|
|
166
|
+
<span className="text-sm font-medium">{fieldKey}</span>
|
|
167
|
+
<Badge variant="outline" className="text-[10px] px-1 py-0">number</Badge>
|
|
168
|
+
</div>
|
|
169
|
+
<Input
|
|
170
|
+
type="number"
|
|
171
|
+
value={value}
|
|
172
|
+
onChange={e => onChange(Number(e.target.value))}
|
|
173
|
+
className="h-8 text-sm"
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (valueType === 'string') {
|
|
180
|
+
const isMultiline = value.includes('\n') || value.length > 80
|
|
181
|
+
return (
|
|
182
|
+
<div className="p-3 rounded-lg bg-muted/50 border space-y-1.5">
|
|
183
|
+
<div className="flex items-center gap-1.5">
|
|
184
|
+
<span className="text-sm font-medium">{fieldKey}</span>
|
|
185
|
+
<Badge variant="outline" className="text-[10px] px-1 py-0">string</Badge>
|
|
186
|
+
</div>
|
|
187
|
+
{isMultiline ? (
|
|
188
|
+
<Textarea
|
|
189
|
+
value={value}
|
|
190
|
+
onChange={e => onChange(e.target.value)}
|
|
191
|
+
className="text-sm font-mono min-h-[80px]"
|
|
192
|
+
/>
|
|
193
|
+
) : (
|
|
194
|
+
<Input
|
|
195
|
+
value={value}
|
|
196
|
+
onChange={e => onChange(e.target.value)}
|
|
197
|
+
className="h-8 text-sm"
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (Array.isArray(value)) {
|
|
205
|
+
return (
|
|
206
|
+
<div className="p-3 rounded-lg bg-muted/50 border space-y-1.5">
|
|
207
|
+
<div className="flex items-center gap-1.5">
|
|
208
|
+
<span className="text-sm font-medium">{fieldKey}</span>
|
|
209
|
+
<Badge variant="outline" className="text-[10px] px-1 py-0">array[{value.length}]</Badge>
|
|
210
|
+
</div>
|
|
211
|
+
<Textarea
|
|
212
|
+
value={stringifyYaml(value).trim()}
|
|
213
|
+
onChange={e => {
|
|
214
|
+
try {
|
|
215
|
+
const parsed = parseYaml(e.target.value)
|
|
216
|
+
if (Array.isArray(parsed)) onChange(parsed)
|
|
217
|
+
} catch { /* ignore parse errors during typing */ }
|
|
218
|
+
}}
|
|
219
|
+
className="text-sm font-mono min-h-[80px]"
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (valueType === 'object') {
|
|
226
|
+
return (
|
|
227
|
+
<div className="p-3 rounded-lg bg-muted/50 border space-y-1.5">
|
|
228
|
+
<div className="flex items-center gap-1.5">
|
|
229
|
+
<span className="text-sm font-medium">{fieldKey}</span>
|
|
230
|
+
<Badge variant="outline" className="text-[10px] px-1 py-0">object</Badge>
|
|
231
|
+
</div>
|
|
232
|
+
<Textarea
|
|
233
|
+
value={stringifyYaml(value).trim()}
|
|
234
|
+
onChange={e => {
|
|
235
|
+
try {
|
|
236
|
+
const parsed = parseYaml(e.target.value)
|
|
237
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) onChange(parsed)
|
|
238
|
+
} catch { /* ignore parse errors during typing */ }
|
|
239
|
+
}}
|
|
240
|
+
className="text-sm font-mono min-h-[100px]"
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<div className="p-3 rounded-lg bg-muted/50 border space-y-1.5">
|
|
248
|
+
<div className="flex items-center gap-1.5">
|
|
249
|
+
<span className="text-sm font-medium">{fieldKey}</span>
|
|
250
|
+
<Badge variant="outline" className="text-[10px] px-1 py-0">{valueType}</Badge>
|
|
251
|
+
</div>
|
|
252
|
+
<Input
|
|
253
|
+
value={String(value)}
|
|
254
|
+
onChange={e => onChange(e.target.value)}
|
|
255
|
+
className="h-8 text-sm"
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export default function DashboardConfig() {
|
|
262
|
+
const { yaml, pluginKeys, loading, error, load, save } = useConfigYaml()
|
|
263
|
+
const [mode, setMode] = useState<'form' | 'yaml'>('form')
|
|
264
|
+
const [yamlText, setYamlText] = useState('')
|
|
265
|
+
const [yamlDirty, setYamlDirty] = useState(false)
|
|
266
|
+
const [saving, setSaving] = useState(false)
|
|
267
|
+
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
|
268
|
+
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
if (yaml) {
|
|
271
|
+
setYamlText(yaml)
|
|
272
|
+
setYamlDirty(false)
|
|
273
|
+
}
|
|
274
|
+
}, [yaml])
|
|
275
|
+
|
|
276
|
+
const parsedConfig = useMemo(() => {
|
|
277
|
+
try {
|
|
278
|
+
return parseYaml(yaml) || {}
|
|
279
|
+
} catch {
|
|
280
|
+
return {}
|
|
281
|
+
}
|
|
282
|
+
}, [yaml])
|
|
283
|
+
|
|
284
|
+
const showMessage = useCallback((type: 'success' | 'error', text: string) => {
|
|
285
|
+
setMessage({ type, text })
|
|
286
|
+
setTimeout(() => setMessage(null), 3000)
|
|
287
|
+
}, [])
|
|
288
|
+
|
|
289
|
+
const handleYamlSave = async () => {
|
|
290
|
+
setSaving(true)
|
|
291
|
+
try {
|
|
292
|
+
await save(yamlText)
|
|
293
|
+
setYamlDirty(false)
|
|
294
|
+
showMessage('success', '配置已保存,需重启生效')
|
|
295
|
+
} catch (err) {
|
|
296
|
+
showMessage('error', `保存失败: ${err instanceof Error ? err.message : '未知错误'}`)
|
|
297
|
+
} finally {
|
|
298
|
+
setSaving(false)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const handleFormSave = async (patch: Record<string, any>) => {
|
|
303
|
+
setSaving(true)
|
|
304
|
+
try {
|
|
305
|
+
const currentParsed = parseYaml(yaml) || {}
|
|
306
|
+
const merged = { ...currentParsed, ...patch }
|
|
307
|
+
const newYaml = stringifyYaml(merged, { lineWidth: 0 })
|
|
308
|
+
await save(newYaml)
|
|
309
|
+
showMessage('success', '配置已保存,需重启生效')
|
|
310
|
+
} catch (err) {
|
|
311
|
+
showMessage('error', `保存失败: ${err instanceof Error ? err.message : '未知错误'}`)
|
|
312
|
+
} finally {
|
|
313
|
+
setSaving(false)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const handleRefresh = async () => {
|
|
318
|
+
try {
|
|
319
|
+
await load()
|
|
320
|
+
showMessage('success', '已刷新')
|
|
321
|
+
} catch {
|
|
322
|
+
showMessage('error', '刷新失败')
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (loading && !yaml) {
|
|
327
|
+
return (
|
|
328
|
+
<div className="space-y-4">
|
|
329
|
+
<h1 className="text-2xl font-bold tracking-tight">配置管理</h1>
|
|
330
|
+
<div className="flex items-center justify-center py-12">
|
|
331
|
+
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
|
332
|
+
<span className="ml-2 text-sm text-muted-foreground">加载配置中...</span>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<div className="space-y-4">
|
|
340
|
+
<div className="flex items-center justify-between">
|
|
341
|
+
<div>
|
|
342
|
+
<h1 className="text-2xl font-bold tracking-tight">配置管理</h1>
|
|
343
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
344
|
+
管理 zhin.config.yml 中的通用配置项(不含插件配置)
|
|
345
|
+
</p>
|
|
346
|
+
</div>
|
|
347
|
+
<div className="flex items-center gap-2">
|
|
348
|
+
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={loading}>
|
|
349
|
+
<RefreshCw className={`w-4 h-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
|
|
350
|
+
刷新
|
|
351
|
+
</Button>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
{message && (
|
|
356
|
+
<Alert variant={message.type === 'error' ? 'destructive' : 'success'} className="py-2">
|
|
357
|
+
{message.type === 'error'
|
|
358
|
+
? <AlertCircle className="h-4 w-4" />
|
|
359
|
+
: <CheckCircle className="h-4 w-4" />}
|
|
360
|
+
<AlertDescription>{message.text}</AlertDescription>
|
|
361
|
+
</Alert>
|
|
362
|
+
)}
|
|
363
|
+
|
|
364
|
+
{error && !message && (
|
|
365
|
+
<Alert variant="destructive" className="py-2">
|
|
366
|
+
<AlertCircle className="h-4 w-4" />
|
|
367
|
+
<AlertDescription>{error}</AlertDescription>
|
|
368
|
+
</Alert>
|
|
369
|
+
)}
|
|
370
|
+
|
|
371
|
+
<Tabs value={mode} onValueChange={v => setMode(v as 'form' | 'yaml')}>
|
|
372
|
+
<TabsList>
|
|
373
|
+
<TabsTrigger value="form" className="gap-1.5">
|
|
374
|
+
<FormInput className="w-4 h-4" />
|
|
375
|
+
表单模式
|
|
376
|
+
</TabsTrigger>
|
|
377
|
+
<TabsTrigger value="yaml" className="gap-1.5">
|
|
378
|
+
<FileCode className="w-4 h-4" />
|
|
379
|
+
YAML 模式
|
|
380
|
+
</TabsTrigger>
|
|
381
|
+
</TabsList>
|
|
382
|
+
|
|
383
|
+
<TabsContent value="form">
|
|
384
|
+
<GeneralConfigForm
|
|
385
|
+
config={parsedConfig}
|
|
386
|
+
pluginKeys={pluginKeys}
|
|
387
|
+
onSave={handleFormSave}
|
|
388
|
+
saving={saving}
|
|
389
|
+
/>
|
|
390
|
+
</TabsContent>
|
|
391
|
+
|
|
392
|
+
<TabsContent value="yaml">
|
|
393
|
+
<div className="space-y-3">
|
|
394
|
+
<div className="relative">
|
|
395
|
+
<Textarea
|
|
396
|
+
value={yamlText}
|
|
397
|
+
onChange={e => { setYamlText(e.target.value); setYamlDirty(true) }}
|
|
398
|
+
className="font-mono text-sm min-h-[400px] resize-y"
|
|
399
|
+
placeholder="# zhin.config.yml"
|
|
400
|
+
spellCheck={false}
|
|
401
|
+
/>
|
|
402
|
+
</div>
|
|
403
|
+
<div className="flex items-center gap-2">
|
|
404
|
+
<Button size="sm" onClick={handleYamlSave} disabled={saving || !yamlDirty}>
|
|
405
|
+
{saving
|
|
406
|
+
? <><Loader2 className="w-4 h-4 mr-1 animate-spin" />保存中...</>
|
|
407
|
+
: <><Save className="w-4 h-4 mr-1" />保存</>}
|
|
408
|
+
</Button>
|
|
409
|
+
{yamlDirty && (
|
|
410
|
+
<Button variant="outline" size="sm" onClick={() => { setYamlText(yaml); setYamlDirty(false) }}>
|
|
411
|
+
<X className="w-4 h-4 mr-1" />撤销
|
|
412
|
+
</Button>
|
|
413
|
+
)}
|
|
414
|
+
{yamlDirty && <span className="text-xs text-muted-foreground">有未保存的更改</span>}
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
</TabsContent>
|
|
418
|
+
</Tabs>
|
|
419
|
+
</div>
|
|
420
|
+
)
|
|
421
|
+
}
|