@zhin.js/console 1.0.20 → 1.0.22
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 +24 -0
- package/README.md +4 -4
- package/client/components.json +17 -0
- package/client/index.html +1 -1
- package/client/src/components/PluginConfigForm/BasicFieldRenderers.tsx +89 -180
- package/client/src/components/PluginConfigForm/CollectionFieldRenderers.tsx +97 -200
- package/client/src/components/PluginConfigForm/CompositeFieldRenderers.tsx +31 -70
- package/client/src/components/PluginConfigForm/FieldRenderer.tsx +27 -77
- package/client/src/components/PluginConfigForm/NestedFieldRenderer.tsx +33 -53
- package/client/src/components/PluginConfigForm/index.tsx +71 -173
- package/client/src/components/ui/accordion.tsx +54 -0
- package/client/src/components/ui/alert.tsx +62 -0
- package/client/src/components/ui/avatar.tsx +41 -0
- package/client/src/components/ui/badge.tsx +32 -0
- package/client/src/components/ui/button.tsx +50 -0
- package/client/src/components/ui/card.tsx +50 -0
- package/client/src/components/ui/checkbox.tsx +25 -0
- package/client/src/components/ui/dialog.tsx +87 -0
- package/client/src/components/ui/dropdown-menu.tsx +97 -0
- package/client/src/components/ui/input.tsx +21 -0
- package/client/src/components/ui/scroll-area.tsx +43 -0
- package/client/src/components/ui/select.tsx +127 -0
- package/client/src/components/ui/separator.tsx +23 -0
- package/client/src/components/ui/skeleton.tsx +12 -0
- package/client/src/components/ui/switch.tsx +26 -0
- package/client/src/components/ui/tabs.tsx +52 -0
- package/client/src/components/ui/textarea.tsx +20 -0
- package/client/src/components/ui/tooltip.tsx +27 -0
- package/client/src/layouts/dashboard.tsx +91 -221
- package/client/src/main.tsx +38 -42
- package/client/src/pages/dashboard-bots.tsx +91 -137
- package/client/src/pages/dashboard-home.tsx +133 -204
- package/client/src/pages/dashboard-logs.tsx +125 -196
- package/client/src/pages/dashboard-plugin-detail.tsx +261 -329
- package/client/src/pages/dashboard-plugins.tsx +108 -105
- package/client/src/style.css +156 -865
- package/client/src/theme/index.ts +60 -35
- package/client/tailwind.config.js +78 -69
- package/dist/client.js +1 -1
- package/dist/cva.js +47 -0
- package/dist/index.html +1 -1
- package/dist/index.js +6 -6
- package/dist/react-router.js +7121 -5585
- package/dist/react.js +192 -149
- package/dist/style.css +2 -2
- package/lib/bin.js +2 -2
- package/lib/build.js +2 -2
- package/lib/index.d.ts +0 -3
- package/lib/index.js +160 -205
- package/lib/transform.d.ts +26 -0
- package/lib/transform.js +78 -0
- package/lib/websocket.d.ts +0 -1
- package/package.json +8 -7
- package/dist/radix-ui-themes.js +0 -9305
- package/lib/dev.d.ts +0 -18
- package/lib/dev.js +0 -87
|
@@ -1,176 +1,108 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* 处理: list, tuple, object, dict
|
|
2
|
+
* Collection type field renderers
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
|
-
import { Flex, Box, Text, TextArea, Button, Badge, Card, Separator } from '@radix-ui/themes'
|
|
7
5
|
import { List, Trash2, Plus, Package, Code, Info } from 'lucide-react'
|
|
8
6
|
import type { FieldRendererProps, SchemaField } from './types.js'
|
|
7
|
+
import { Textarea } from '../ui/textarea'
|
|
8
|
+
import { Badge } from '../ui/badge'
|
|
9
|
+
import { Button } from '../ui/button'
|
|
10
|
+
import { Card } from '../ui/card'
|
|
11
|
+
import { Separator } from '../ui/separator'
|
|
9
12
|
|
|
10
13
|
interface CollectionFieldProps extends FieldRendererProps {
|
|
11
|
-
renderNestedField: (
|
|
12
|
-
fieldName: string,
|
|
13
|
-
field: SchemaField,
|
|
14
|
-
value: any,
|
|
15
|
-
onChange: (val: any) => void
|
|
16
|
-
) => React.ReactElement
|
|
14
|
+
renderNestedField: (fieldName: string, field: SchemaField, value: any, onChange: (val: any) => void) => React.ReactElement
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
export function ListFieldRenderer({
|
|
20
|
-
fieldName,
|
|
21
|
-
field,
|
|
22
|
-
value,
|
|
23
|
-
onChange,
|
|
24
|
-
onArrayItemChange,
|
|
25
|
-
renderNestedField
|
|
17
|
+
export function ListFieldRenderer({
|
|
18
|
+
fieldName, field, value, onChange, onArrayItemChange, renderNestedField
|
|
26
19
|
}: CollectionFieldProps) {
|
|
27
20
|
const arrayValue = Array.isArray(value) ? value : []
|
|
28
21
|
const innerField = field.inner || field.items
|
|
29
|
-
|
|
30
|
-
// 简单类型 - 使用多行文本 - 优化样式
|
|
22
|
+
|
|
31
23
|
if (innerField && ['string', 'number'].includes(innerField.type)) {
|
|
32
24
|
return (
|
|
33
|
-
<div className="p-3 rounded-lg bg-
|
|
34
|
-
<
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const parsed = innerField.type === 'number'
|
|
47
|
-
? lines.map(l => parseFloat(l) || 0)
|
|
48
|
-
: lines
|
|
49
|
-
onChange(parsed)
|
|
50
|
-
}}
|
|
51
|
-
placeholder={`每行一个值\n${field.description || ''}`}
|
|
52
|
-
rows={4}
|
|
53
|
-
className="font-mono text-sm bg-white dark:bg-gray-950 hover:border-green-500 dark:hover:border-green-400 transition-colors"
|
|
54
|
-
/>
|
|
55
|
-
</Flex>
|
|
25
|
+
<div className="p-3 rounded-lg bg-muted/50 border space-y-2">
|
|
26
|
+
<p className="flex items-center gap-1 text-xs font-semibold text-muted-foreground">
|
|
27
|
+
<List className="w-3 h-3" /> 列表输入 (每行一个值)
|
|
28
|
+
</p>
|
|
29
|
+
<Textarea
|
|
30
|
+
value={arrayValue.join('\n')}
|
|
31
|
+
onChange={(e) => {
|
|
32
|
+
const lines = e.target.value.split('\n').filter(Boolean)
|
|
33
|
+
onChange(innerField.type === 'number' ? lines.map(l => parseFloat(l) || 0) : lines)
|
|
34
|
+
}}
|
|
35
|
+
placeholder={`每行一个值\n${field.description || ''}`}
|
|
36
|
+
rows={4} className="font-mono text-sm"
|
|
37
|
+
/>
|
|
56
38
|
</div>
|
|
57
39
|
)
|
|
58
40
|
}
|
|
59
|
-
|
|
60
|
-
// 复杂类型 - Card 列表 - 优化样式
|
|
41
|
+
|
|
61
42
|
return (
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
{
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}}
|
|
86
|
-
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
|
87
|
-
>
|
|
88
|
-
<Trash2 className="w-3 h-3" />
|
|
89
|
-
</Button>
|
|
90
|
-
</Flex>
|
|
91
|
-
<div className="pl-2 border-l-2 border-blue-200 dark:border-blue-800">
|
|
92
|
-
{innerField && renderNestedField(`${fieldName}[${index}]`, innerField, item, (val) => {
|
|
93
|
-
onArrayItemChange?.(fieldName, index, val)
|
|
94
|
-
})}
|
|
95
|
-
</div>
|
|
96
|
-
</Flex>
|
|
97
|
-
</Card>
|
|
98
|
-
))}
|
|
99
|
-
</div>
|
|
100
|
-
<Button
|
|
101
|
-
size="2"
|
|
102
|
-
variant="soft"
|
|
103
|
-
onClick={() => {
|
|
104
|
-
const newItem = innerField?.default || (innerField?.type === 'object' ? {} : '')
|
|
105
|
-
onChange([...arrayValue, newItem])
|
|
106
|
-
}}
|
|
107
|
-
className="w-full hover:bg-blue-100 dark:hover:bg-blue-900/30 border-2 border-dashed border-blue-300 dark:border-blue-700 hover:border-blue-500 dark:hover:border-blue-500 transition-colors"
|
|
43
|
+
<div className="space-y-2">
|
|
44
|
+
{arrayValue.map((item, index) => (
|
|
45
|
+
<Card key={index} className="group">
|
|
46
|
+
<div className="p-3 space-y-2">
|
|
47
|
+
<div className="flex justify-between items-center">
|
|
48
|
+
<Badge variant="secondary" className="font-mono">{index + 1}</Badge>
|
|
49
|
+
<Button variant="ghost" size="sm"
|
|
50
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity text-destructive"
|
|
51
|
+
onClick={() => onChange(arrayValue.filter((_, i) => i !== index))}
|
|
52
|
+
>
|
|
53
|
+
<Trash2 className="w-3 h-3" />
|
|
54
|
+
</Button>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="pl-2 border-l-2">
|
|
57
|
+
{innerField && renderNestedField(`${fieldName}[${index}]`, innerField, item, (val) => {
|
|
58
|
+
onArrayItemChange?.(fieldName, index, val)
|
|
59
|
+
})}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</Card>
|
|
63
|
+
))}
|
|
64
|
+
<Button variant="outline" size="sm" className="w-full border-dashed"
|
|
65
|
+
onClick={() => onChange([...arrayValue, innerField?.default || (innerField?.type === 'object' ? {} : '')])}
|
|
108
66
|
>
|
|
109
|
-
<Plus className="w-4 h-4" />
|
|
110
|
-
添加项
|
|
67
|
+
<Plus className="w-4 h-4 mr-1" /> 添加项
|
|
111
68
|
</Button>
|
|
112
|
-
</
|
|
69
|
+
</div>
|
|
113
70
|
)
|
|
114
71
|
}
|
|
115
72
|
|
|
116
73
|
export function ArrayFieldRenderer({ field, value, onChange }: FieldRendererProps) {
|
|
117
|
-
// 兼容旧格式 - 优化样式
|
|
118
74
|
return (
|
|
119
|
-
<div className="p-3 rounded-lg bg-
|
|
120
|
-
<
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
value={Array.isArray(value) ? value.join('\n') : ''}
|
|
130
|
-
onChange={(e) => onChange(e.target.value.split('\n').filter(Boolean))}
|
|
131
|
-
placeholder={`每行一个值\n${field.description || ''}`}
|
|
132
|
-
rows={3}
|
|
133
|
-
className="font-mono text-sm bg-white dark:bg-gray-950 hover:border-cyan-500 dark:hover:border-cyan-400 transition-colors"
|
|
134
|
-
/>
|
|
135
|
-
</Flex>
|
|
75
|
+
<div className="p-3 rounded-lg bg-muted/50 border space-y-2">
|
|
76
|
+
<p className="flex items-center gap-1 text-xs font-semibold text-muted-foreground">
|
|
77
|
+
<List className="w-3 h-3" /> 数组输入 (每行一个值)
|
|
78
|
+
</p>
|
|
79
|
+
<Textarea
|
|
80
|
+
value={Array.isArray(value) ? value.join('\n') : ''}
|
|
81
|
+
onChange={(e) => onChange(e.target.value.split('\n').filter(Boolean))}
|
|
82
|
+
placeholder={`每行一个值\n${field.description || ''}`}
|
|
83
|
+
rows={3} className="font-mono text-sm"
|
|
84
|
+
/>
|
|
136
85
|
</div>
|
|
137
86
|
)
|
|
138
87
|
}
|
|
139
88
|
|
|
140
89
|
export function TupleFieldRenderer({
|
|
141
|
-
fieldName,
|
|
142
|
-
field,
|
|
143
|
-
value,
|
|
144
|
-
onArrayItemChange,
|
|
145
|
-
renderNestedField
|
|
90
|
+
fieldName, field, value, onArrayItemChange, renderNestedField
|
|
146
91
|
}: CollectionFieldProps) {
|
|
147
92
|
const tupleValue = Array.isArray(value) ? value : []
|
|
148
93
|
const tupleFields = field.list || []
|
|
149
|
-
|
|
94
|
+
|
|
150
95
|
return (
|
|
151
|
-
<div className="space-y-
|
|
96
|
+
<div className="space-y-2">
|
|
152
97
|
{tupleFields.map((tupleField, index) => (
|
|
153
|
-
<div
|
|
154
|
-
|
|
155
|
-
className="
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
</Badge>
|
|
162
|
-
</Flex>
|
|
163
|
-
{tupleField.description && (
|
|
164
|
-
<Text size="1" className="text-indigo-700 dark:text-indigo-300">
|
|
165
|
-
{tupleField.description}
|
|
166
|
-
</Text>
|
|
167
|
-
)}
|
|
168
|
-
<div className="mt-1">
|
|
169
|
-
{renderNestedField(`${fieldName}[${index}]`, tupleField, tupleValue[index], (val) => {
|
|
170
|
-
onArrayItemChange?.(fieldName, index, val)
|
|
171
|
-
})}
|
|
172
|
-
</div>
|
|
173
|
-
</Flex>
|
|
98
|
+
<div key={index} className="p-3 rounded-lg bg-muted/50 border space-y-2">
|
|
99
|
+
<Badge variant="secondary" className="font-mono">#{index + 1}</Badge>
|
|
100
|
+
{tupleField.description && <p className="text-xs text-muted-foreground">{tupleField.description}</p>}
|
|
101
|
+
<div className="mt-1">
|
|
102
|
+
{renderNestedField(`${fieldName}[${index}]`, tupleField, tupleValue[index], (val) => {
|
|
103
|
+
onArrayItemChange?.(fieldName, index, val)
|
|
104
|
+
})}
|
|
105
|
+
</div>
|
|
174
106
|
</div>
|
|
175
107
|
))}
|
|
176
108
|
</div>
|
|
@@ -178,48 +110,28 @@ export function TupleFieldRenderer({
|
|
|
178
110
|
}
|
|
179
111
|
|
|
180
112
|
export function ObjectFieldRenderer({
|
|
181
|
-
fieldName,
|
|
182
|
-
field,
|
|
183
|
-
value,
|
|
184
|
-
renderField
|
|
113
|
+
fieldName, field, renderField
|
|
185
114
|
}: CollectionFieldProps & { renderField: (fieldName: string, field: SchemaField, parentPath?: string) => React.ReactElement }) {
|
|
186
115
|
const objectFields = field.dict || field.properties || {}
|
|
187
|
-
|
|
116
|
+
|
|
188
117
|
return (
|
|
189
|
-
<div className="rounded-lg border-2
|
|
190
|
-
<div className="px-4 py-2 bg-
|
|
191
|
-
<
|
|
192
|
-
|
|
193
|
-
</Flex>
|
|
118
|
+
<div className="rounded-lg border-2 bg-muted/30 overflow-hidden">
|
|
119
|
+
<div className="px-4 py-2 bg-muted border-b flex items-center gap-2">
|
|
120
|
+
<Package className="w-4 h-4 text-muted-foreground" />
|
|
121
|
+
<span className="text-xs font-semibold text-muted-foreground">对象</span>
|
|
194
122
|
</div>
|
|
195
|
-
<div className="p-
|
|
123
|
+
<div className="p-3 space-y-2">
|
|
196
124
|
{Object.entries(objectFields).map(([key, nestedField]: [string, any], index) => (
|
|
197
125
|
<div key={key}>
|
|
198
|
-
<div className="p-3 rounded-md bg-
|
|
199
|
-
<
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<Text size="2" weight="bold" color="red" className="leading-none">
|
|
206
|
-
*
|
|
207
|
-
</Text>
|
|
208
|
-
)}
|
|
209
|
-
</Flex>
|
|
210
|
-
{nestedField.description && (
|
|
211
|
-
<Text size="1" color="gray">
|
|
212
|
-
{nestedField.description}
|
|
213
|
-
</Text>
|
|
214
|
-
)}
|
|
215
|
-
<div className="mt-1">
|
|
216
|
-
{renderField(key, nestedField, fieldName)}
|
|
217
|
-
</div>
|
|
218
|
-
</Flex>
|
|
126
|
+
<div className="p-3 rounded-md bg-background border space-y-1">
|
|
127
|
+
<div className="flex items-center gap-1">
|
|
128
|
+
<span className="text-sm font-semibold">{nestedField.key || key}</span>
|
|
129
|
+
{nestedField.required && <span className="text-destructive font-bold">*</span>}
|
|
130
|
+
</div>
|
|
131
|
+
{nestedField.description && <p className="text-xs text-muted-foreground">{nestedField.description}</p>}
|
|
132
|
+
<div className="mt-1">{renderField(key, nestedField, fieldName)}</div>
|
|
219
133
|
</div>
|
|
220
|
-
{index < Object.entries(objectFields).length - 1 &&
|
|
221
|
-
<Separator size="4" className="my-2" />
|
|
222
|
-
)}
|
|
134
|
+
{index < Object.entries(objectFields).length - 1 && <Separator className="my-2" />}
|
|
223
135
|
</div>
|
|
224
136
|
))}
|
|
225
137
|
</div>
|
|
@@ -229,33 +141,18 @@ export function ObjectFieldRenderer({
|
|
|
229
141
|
|
|
230
142
|
export function DictFieldRenderer({ field, value, onChange }: FieldRendererProps) {
|
|
231
143
|
return (
|
|
232
|
-
<div className="p-3 rounded-lg bg-
|
|
233
|
-
<
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
} catch {
|
|
245
|
-
// 忽略解析错误,继续编辑
|
|
246
|
-
}
|
|
247
|
-
}}
|
|
248
|
-
placeholder={field.description || '请输入 JSON 格式'}
|
|
249
|
-
rows={6}
|
|
250
|
-
className="font-mono text-sm bg-white dark:bg-gray-950 hover:border-violet-500 dark:hover:border-violet-400 transition-colors"
|
|
251
|
-
/>
|
|
252
|
-
<Flex align="center" gap="2">
|
|
253
|
-
<Info className="w-3 h-3 text-violet-600 dark:text-violet-400" />
|
|
254
|
-
<Text size="1" className="text-violet-700 dark:text-violet-300">
|
|
255
|
-
键值对格式: {"key": "value"}
|
|
256
|
-
</Text>
|
|
257
|
-
</Flex>
|
|
258
|
-
</Flex>
|
|
144
|
+
<div className="p-3 rounded-lg bg-muted/50 border space-y-2">
|
|
145
|
+
<p className="flex items-center gap-1 text-xs font-semibold text-muted-foreground">
|
|
146
|
+
<Code className="w-3 h-3" /> 键值对编辑
|
|
147
|
+
</p>
|
|
148
|
+
<Textarea
|
|
149
|
+
value={typeof value === 'object' ? JSON.stringify(value, null, 2) : value || '{}'}
|
|
150
|
+
onChange={(e) => { try { onChange(JSON.parse(e.target.value)) } catch {} }}
|
|
151
|
+
placeholder={field.description || '请输入 JSON 格式'} rows={6} className="font-mono text-sm"
|
|
152
|
+
/>
|
|
153
|
+
<p className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
154
|
+
<Info className="w-3 h-3" /> 键值对格式: {"{"}"key": "value"{"}"}
|
|
155
|
+
</p>
|
|
259
156
|
</div>
|
|
260
157
|
)
|
|
261
158
|
}
|
|
@@ -1,102 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* 处理: union, intersect
|
|
2
|
+
* Composite type field renderers
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
|
-
import { Flex, Box, Text, TextField, Select, Card, Badge } from '@radix-ui/themes'
|
|
7
5
|
import { GitBranch, Layers } from 'lucide-react'
|
|
8
6
|
import type { FieldRendererProps, SchemaField } from './types.js'
|
|
7
|
+
import { Input } from '../ui/input'
|
|
8
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
|
9
9
|
|
|
10
10
|
interface CompositeFieldProps extends FieldRendererProps {
|
|
11
|
-
renderNestedField: (
|
|
12
|
-
fieldName: string,
|
|
13
|
-
field: SchemaField,
|
|
14
|
-
value: any,
|
|
15
|
-
onChange: (val: any) => void
|
|
16
|
-
) => React.ReactElement
|
|
11
|
+
renderNestedField: (fieldName: string, field: SchemaField, value: any, onChange: (val: any) => void) => React.ReactElement
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
export function UnionFieldRenderer({ field, value, onChange }: FieldRendererProps) {
|
|
20
15
|
const unionFields = field.list || []
|
|
21
|
-
|
|
16
|
+
|
|
22
17
|
if (unionFields.length === 0) {
|
|
23
18
|
return (
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
value={value || ''}
|
|
27
|
-
onChange={(e) => onChange(e.target.value)}
|
|
19
|
+
<Input
|
|
20
|
+
value={value || ''} onChange={(e) => onChange(e.target.value)}
|
|
28
21
|
placeholder={field.description || '请输入值'}
|
|
29
|
-
className="hover:border-blue-500 dark:hover:border-blue-400 transition-colors focus-within:ring-2 focus-within:ring-blue-500/20"
|
|
30
22
|
/>
|
|
31
23
|
)
|
|
32
24
|
}
|
|
33
|
-
|
|
34
|
-
// 如果所有选项都是简单类型,使用下拉选择 - 优化样式
|
|
25
|
+
|
|
35
26
|
const options = unionFields.map((uf: any) => uf.default || uf.type)
|
|
36
|
-
|
|
27
|
+
|
|
37
28
|
return (
|
|
38
|
-
<div className="p-3 rounded-lg bg-
|
|
39
|
-
<
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{options.map((option: any, index: number) => (
|
|
51
|
-
<Select.Item
|
|
52
|
-
key={index}
|
|
53
|
-
value={String(option)}
|
|
54
|
-
className="hover:bg-pink-50 dark:hover:bg-pink-900/20 transition-colors"
|
|
55
|
-
>
|
|
56
|
-
<Flex align="center" gap="2">
|
|
57
|
-
<Text>{String(option)}</Text>
|
|
58
|
-
</Flex>
|
|
59
|
-
</Select.Item>
|
|
60
|
-
))}
|
|
61
|
-
</Select.Content>
|
|
62
|
-
</Select.Root>
|
|
63
|
-
</Flex>
|
|
29
|
+
<div className="p-3 rounded-lg bg-muted/50 border space-y-2">
|
|
30
|
+
<p className="flex items-center gap-1 text-xs font-semibold text-muted-foreground">
|
|
31
|
+
<GitBranch className="w-3 h-3" /> 联合类型
|
|
32
|
+
</p>
|
|
33
|
+
<Select value={value?.toString() || ''} onValueChange={onChange}>
|
|
34
|
+
<SelectTrigger><SelectValue placeholder="请选择" /></SelectTrigger>
|
|
35
|
+
<SelectContent>
|
|
36
|
+
{options.map((option: any, index: number) => (
|
|
37
|
+
<SelectItem key={index} value={String(option)}>{String(option)}</SelectItem>
|
|
38
|
+
))}
|
|
39
|
+
</SelectContent>
|
|
40
|
+
</Select>
|
|
64
41
|
</div>
|
|
65
42
|
)
|
|
66
43
|
}
|
|
67
44
|
|
|
68
45
|
export function IntersectFieldRenderer({
|
|
69
|
-
fieldName,
|
|
70
|
-
field,
|
|
71
|
-
value,
|
|
72
|
-
onChange,
|
|
73
|
-
renderNestedField
|
|
46
|
+
fieldName, field, value, onChange, renderNestedField
|
|
74
47
|
}: CompositeFieldProps) {
|
|
75
48
|
const intersectFields = field.list || []
|
|
76
|
-
|
|
49
|
+
|
|
77
50
|
return (
|
|
78
|
-
<div className="rounded-lg border-2
|
|
79
|
-
<div className="px-4 py-2 bg-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
</Flex>
|
|
51
|
+
<div className="rounded-lg border-2 bg-muted/30 overflow-hidden">
|
|
52
|
+
<div className="px-4 py-2 bg-muted border-b flex items-center gap-2">
|
|
53
|
+
<Layers className="w-4 h-4 text-muted-foreground" />
|
|
54
|
+
<span className="text-xs font-semibold text-muted-foreground">交叉类型</span>
|
|
83
55
|
</div>
|
|
84
|
-
<div className="p-
|
|
56
|
+
<div className="p-3 space-y-2">
|
|
85
57
|
{intersectFields.map((iField: any, index: number) => (
|
|
86
|
-
<div
|
|
87
|
-
|
|
88
|
-
className="
|
|
89
|
-
>
|
|
90
|
-
<Flex direction="column" gap="2">
|
|
91
|
-
{iField.description && (
|
|
92
|
-
<Text size="1" color="gray">
|
|
93
|
-
{iField.description}
|
|
94
|
-
</Text>
|
|
95
|
-
)}
|
|
96
|
-
<div className="mt-1">
|
|
97
|
-
{renderNestedField(`${fieldName}[${index}]`, iField, value, onChange)}
|
|
98
|
-
</div>
|
|
99
|
-
</Flex>
|
|
58
|
+
<div key={index} className="p-3 rounded-md bg-background border space-y-1">
|
|
59
|
+
{iField.description && <p className="text-xs text-muted-foreground">{iField.description}</p>}
|
|
60
|
+
<div className="mt-1">{renderNestedField(`${fieldName}[${index}]`, iField, value, onChange)}</div>
|
|
100
61
|
</div>
|
|
101
62
|
))}
|
|
102
63
|
</div>
|
|
@@ -1,31 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* 根据字段类型分发到对应的渲染器
|
|
2
|
+
* Field renderer - dispatches to type-specific renderers
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
|
-
import { TextArea } from '@radix-ui/themes'
|
|
7
5
|
import type { FieldRendererProps, SchemaField } from './types.js'
|
|
6
|
+
import { Textarea } from '../ui/textarea'
|
|
8
7
|
import {
|
|
9
|
-
StringFieldRenderer,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
PercentFieldRenderer,
|
|
13
|
-
DateFieldRenderer,
|
|
14
|
-
RegexpFieldRenderer,
|
|
15
|
-
ConstFieldRenderer,
|
|
16
|
-
AnyFieldRenderer
|
|
8
|
+
StringFieldRenderer, NumberFieldRenderer, BooleanFieldRenderer,
|
|
9
|
+
PercentFieldRenderer, DateFieldRenderer, RegexpFieldRenderer,
|
|
10
|
+
ConstFieldRenderer, AnyFieldRenderer
|
|
17
11
|
} from './BasicFieldRenderers.js'
|
|
18
12
|
import {
|
|
19
|
-
ListFieldRenderer,
|
|
20
|
-
|
|
21
|
-
TupleFieldRenderer,
|
|
22
|
-
ObjectFieldRenderer,
|
|
23
|
-
DictFieldRenderer
|
|
13
|
+
ListFieldRenderer, ArrayFieldRenderer, TupleFieldRenderer,
|
|
14
|
+
ObjectFieldRenderer, DictFieldRenderer
|
|
24
15
|
} from './CollectionFieldRenderers.js'
|
|
25
|
-
import {
|
|
26
|
-
UnionFieldRenderer,
|
|
27
|
-
IntersectFieldRenderer
|
|
28
|
-
} from './CompositeFieldRenderers.js'
|
|
16
|
+
import { UnionFieldRenderer, IntersectFieldRenderer } from './CompositeFieldRenderers.js'
|
|
29
17
|
|
|
30
18
|
interface FieldRendererConfig extends FieldRendererProps {
|
|
31
19
|
renderField: (fieldName: string, field: SchemaField, parentPath?: string) => React.ReactElement
|
|
@@ -36,66 +24,29 @@ export function FieldRenderer(props: FieldRendererConfig) {
|
|
|
36
24
|
const { field } = props
|
|
37
25
|
|
|
38
26
|
switch (field.type) {
|
|
39
|
-
case 'string':
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
case '
|
|
43
|
-
case '
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
case '
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
case '
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
case '
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
case 'regexp':
|
|
56
|
-
return <RegexpFieldRenderer {...props} />
|
|
57
|
-
|
|
58
|
-
case 'const':
|
|
59
|
-
return <ConstFieldRenderer {...props} />
|
|
60
|
-
case 'any':
|
|
61
|
-
return <AnyFieldRenderer {...props} />
|
|
62
|
-
|
|
63
|
-
case 'list':
|
|
64
|
-
return <ListFieldRenderer {...props} />
|
|
65
|
-
|
|
66
|
-
case 'array':
|
|
67
|
-
return <ArrayFieldRenderer {...props} />
|
|
68
|
-
|
|
69
|
-
case 'tuple':
|
|
70
|
-
return <TupleFieldRenderer {...props} />
|
|
71
|
-
|
|
72
|
-
case 'object':
|
|
73
|
-
return <ObjectFieldRenderer {...props} />
|
|
74
|
-
|
|
75
|
-
case 'dict':
|
|
76
|
-
return <DictFieldRenderer {...props} />
|
|
77
|
-
|
|
78
|
-
case 'union':
|
|
79
|
-
return <UnionFieldRenderer {...props} />
|
|
80
|
-
|
|
81
|
-
case 'intersect':
|
|
82
|
-
return <IntersectFieldRenderer {...props} />
|
|
83
|
-
|
|
27
|
+
case 'string': return <StringFieldRenderer {...props} />
|
|
28
|
+
case 'number': case 'integer': return <NumberFieldRenderer {...props} />
|
|
29
|
+
case 'boolean': return <BooleanFieldRenderer {...props} />
|
|
30
|
+
case 'percent': return <PercentFieldRenderer {...props} />
|
|
31
|
+
case 'date': return <DateFieldRenderer {...props} />
|
|
32
|
+
case 'regexp': return <RegexpFieldRenderer {...props} />
|
|
33
|
+
case 'const': return <ConstFieldRenderer {...props} />
|
|
34
|
+
case 'any': return <AnyFieldRenderer {...props} />
|
|
35
|
+
case 'list': return <ListFieldRenderer {...props} />
|
|
36
|
+
case 'array': return <ArrayFieldRenderer {...props} />
|
|
37
|
+
case 'tuple': return <TupleFieldRenderer {...props} />
|
|
38
|
+
case 'object': return <ObjectFieldRenderer {...props} />
|
|
39
|
+
case 'dict': return <DictFieldRenderer {...props} />
|
|
40
|
+
case 'union': return <UnionFieldRenderer {...props} />
|
|
41
|
+
case 'intersect': return <IntersectFieldRenderer {...props} />
|
|
84
42
|
default:
|
|
85
|
-
// 默认 JSON 编辑器
|
|
86
43
|
return (
|
|
87
|
-
<
|
|
88
|
-
size="1"
|
|
44
|
+
<Textarea
|
|
89
45
|
value={typeof props.value === 'object' ? JSON.stringify(props.value, null, 2) : props.value || ''}
|
|
90
46
|
onChange={(e) => {
|
|
91
|
-
try {
|
|
92
|
-
const parsed = JSON.parse(e.target.value)
|
|
93
|
-
props.onChange(parsed)
|
|
94
|
-
} catch {
|
|
95
|
-
props.onChange(e.target.value)
|
|
96
|
-
}
|
|
47
|
+
try { props.onChange(JSON.parse(e.target.value)) } catch { props.onChange(e.target.value) }
|
|
97
48
|
}}
|
|
98
|
-
placeholder={field.description ||
|
|
49
|
+
placeholder={field.description || '请输入 JSON 格式'}
|
|
99
50
|
rows={4}
|
|
100
51
|
className="font-mono text-xs"
|
|
101
52
|
/>
|
|
@@ -103,8 +54,7 @@ export function FieldRenderer(props: FieldRendererConfig) {
|
|
|
103
54
|
}
|
|
104
55
|
}
|
|
105
56
|
|
|
106
|
-
// 辅助函数:判断字段是否为复杂类型(需要折叠)
|
|
107
57
|
export function isComplexField(field: SchemaField): boolean {
|
|
108
|
-
return ['object', 'list', 'tuple', 'union', 'intersect', 'any'].includes(field.type)
|
|
58
|
+
return ['object', 'list', 'tuple', 'union', 'intersect', 'any'].includes(field.type)
|
|
109
59
|
|| (field.type === 'dict' && !!field.dict)
|
|
110
60
|
}
|