@zhin.js/client 1.0.4 → 1.0.5
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/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- 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 +37 -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 +17 -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 +14 -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 +8 -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}/index.ts +0 -0
- /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}/store/reducers/route.ts +0 -0
- /package/{src → client}/store/reducers/script.ts +0 -0
- /package/{src → client}/store/reducers/ui.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,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 组合类型字段渲染器
|
|
3
|
-
* 处理: union, intersect
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Flex, Box, Text, TextField, Select, Card, Badge } from '@radix-ui/themes'
|
|
7
|
-
import { Icons } from '@zhin.js/client'
|
|
8
|
-
import type { FieldRendererProps, SchemaField } from './types.js'
|
|
9
|
-
|
|
10
|
-
interface CompositeFieldProps extends FieldRendererProps {
|
|
11
|
-
renderNestedField: (
|
|
12
|
-
fieldName: string,
|
|
13
|
-
field: SchemaField,
|
|
14
|
-
value: any,
|
|
15
|
-
onChange: (val: any) => void
|
|
16
|
-
) => React.ReactElement
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function UnionFieldRenderer({ field, value, onChange }: FieldRendererProps) {
|
|
20
|
-
const unionFields = field.list || []
|
|
21
|
-
|
|
22
|
-
if (unionFields.length === 0) {
|
|
23
|
-
return (
|
|
24
|
-
<TextField.Root
|
|
25
|
-
size="2"
|
|
26
|
-
value={value || ''}
|
|
27
|
-
onChange={(e) => onChange(e.target.value)}
|
|
28
|
-
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
|
-
/>
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 如果所有选项都是简单类型,使用下拉选择 - 优化样式
|
|
35
|
-
const options = unionFields.map((uf: any) => uf.default || uf.type)
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div className="p-3 rounded-lg bg-gradient-to-br from-pink-50 to-rose-50 dark:from-pink-900/20 dark:to-rose-900/20 border border-pink-200 dark:border-pink-800">
|
|
39
|
-
<Flex direction="column" gap="2">
|
|
40
|
-
<Flex align="center" gap="2">
|
|
41
|
-
<Icons.GitBranch className="w-4 h-4 text-pink-600 dark:text-pink-400" />
|
|
42
|
-
</Flex>
|
|
43
|
-
<Select.Root
|
|
44
|
-
size="2"
|
|
45
|
-
value={value?.toString() || ''}
|
|
46
|
-
onValueChange={onChange}
|
|
47
|
-
>
|
|
48
|
-
<Select.Trigger className="w-full hover:border-pink-500 dark:hover:border-pink-400 transition-colors" />
|
|
49
|
-
<Select.Content className="shadow-lg">
|
|
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>
|
|
64
|
-
</div>
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function IntersectFieldRenderer({
|
|
69
|
-
fieldName,
|
|
70
|
-
field,
|
|
71
|
-
value,
|
|
72
|
-
onChange,
|
|
73
|
-
renderNestedField
|
|
74
|
-
}: CompositeFieldProps) {
|
|
75
|
-
const intersectFields = field.list || []
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div className="rounded-lg border-2 border-teal-200 dark:border-teal-800 bg-gradient-to-br from-teal-50/50 to-emerald-50/50 dark:from-teal-900/10 dark:to-emerald-900/10 overflow-hidden">
|
|
79
|
-
<div className="px-4 py-2 bg-teal-100 dark:bg-teal-900/30 border-b border-teal-200 dark:border-teal-800">
|
|
80
|
-
<Flex align="center" gap="2">
|
|
81
|
-
<Icons.Layers className="w-4 h-4 text-teal-600 dark:text-teal-400" />
|
|
82
|
-
</Flex>
|
|
83
|
-
</div>
|
|
84
|
-
<div className="p-4 space-y-3">
|
|
85
|
-
{intersectFields.map((iField: any, index: number) => (
|
|
86
|
-
<div
|
|
87
|
-
key={index}
|
|
88
|
-
className="p-3 rounded-md bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800"
|
|
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>
|
|
100
|
-
</div>
|
|
101
|
-
))}
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
)
|
|
105
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 字段渲染器主入口
|
|
3
|
-
* 根据字段类型分发到对应的渲染器
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { TextArea } from '@radix-ui/themes'
|
|
7
|
-
import type { FieldRendererProps, SchemaField } from './types.js'
|
|
8
|
-
import {
|
|
9
|
-
StringFieldRenderer,
|
|
10
|
-
NumberFieldRenderer,
|
|
11
|
-
BooleanFieldRenderer,
|
|
12
|
-
PercentFieldRenderer,
|
|
13
|
-
DateFieldRenderer,
|
|
14
|
-
RegexpFieldRenderer,
|
|
15
|
-
ConstFieldRenderer,
|
|
16
|
-
AnyFieldRenderer
|
|
17
|
-
} from './BasicFieldRenderers.js'
|
|
18
|
-
import {
|
|
19
|
-
ListFieldRenderer,
|
|
20
|
-
ArrayFieldRenderer,
|
|
21
|
-
TupleFieldRenderer,
|
|
22
|
-
ObjectFieldRenderer,
|
|
23
|
-
DictFieldRenderer
|
|
24
|
-
} from './CollectionFieldRenderers.js'
|
|
25
|
-
import {
|
|
26
|
-
UnionFieldRenderer,
|
|
27
|
-
IntersectFieldRenderer
|
|
28
|
-
} from './CompositeFieldRenderers.js'
|
|
29
|
-
|
|
30
|
-
interface FieldRendererConfig extends FieldRendererProps {
|
|
31
|
-
renderField: (fieldName: string, field: SchemaField, parentPath?: string) => React.ReactElement
|
|
32
|
-
renderNestedField: (fieldName: string, field: SchemaField, value: any, onChange: (val: any) => void) => React.ReactElement
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function FieldRenderer(props: FieldRendererConfig) {
|
|
36
|
-
const { field } = props
|
|
37
|
-
|
|
38
|
-
switch (field.type) {
|
|
39
|
-
case 'string':
|
|
40
|
-
return <StringFieldRenderer {...props} />
|
|
41
|
-
|
|
42
|
-
case 'number':
|
|
43
|
-
case 'integer':
|
|
44
|
-
return <NumberFieldRenderer {...props} />
|
|
45
|
-
|
|
46
|
-
case 'boolean':
|
|
47
|
-
return <BooleanFieldRenderer {...props} />
|
|
48
|
-
|
|
49
|
-
case 'percent':
|
|
50
|
-
return <PercentFieldRenderer {...props} />
|
|
51
|
-
|
|
52
|
-
case 'date':
|
|
53
|
-
return <DateFieldRenderer {...props} />
|
|
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
|
-
|
|
84
|
-
default:
|
|
85
|
-
// 默认 JSON 编辑器
|
|
86
|
-
return (
|
|
87
|
-
<TextArea
|
|
88
|
-
size="1"
|
|
89
|
-
value={typeof props.value === 'object' ? JSON.stringify(props.value, null, 2) : props.value || ''}
|
|
90
|
-
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
|
-
}
|
|
97
|
-
}}
|
|
98
|
-
placeholder={field.description || `请输入 JSON 格式`}
|
|
99
|
-
rows={4}
|
|
100
|
-
className="font-mono text-xs"
|
|
101
|
-
/>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// 辅助函数:判断字段是否为复杂类型(需要折叠)
|
|
107
|
-
export function isComplexField(field: SchemaField): boolean {
|
|
108
|
-
return ['object', 'list', 'tuple', 'union', 'intersect', 'any'].includes(field.type)
|
|
109
|
-
|| (field.type === 'dict' && !!field.dict)
|
|
110
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 嵌套字段渲染器
|
|
3
|
-
* 用于渲染数组项、元组项等嵌套字段
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Flex, Box, Text, TextField, TextArea, Switch, Card } from '@radix-ui/themes'
|
|
7
|
-
import type { SchemaField } from './types.js'
|
|
8
|
-
|
|
9
|
-
interface NestedFieldRendererProps {
|
|
10
|
-
fieldName: string
|
|
11
|
-
field: SchemaField
|
|
12
|
-
value: any
|
|
13
|
-
onChange: (val: any) => void
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function NestedFieldRenderer({ field, value, onChange }: NestedFieldRendererProps): React.ReactElement {
|
|
17
|
-
switch (field.type) {
|
|
18
|
-
case 'string':
|
|
19
|
-
return (
|
|
20
|
-
<TextField.Root
|
|
21
|
-
size="1"
|
|
22
|
-
value={value || ''}
|
|
23
|
-
onChange={(e) => onChange(e.target.value)}
|
|
24
|
-
placeholder={field.description || '请输入'}
|
|
25
|
-
/>
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
case 'number':
|
|
29
|
-
case 'integer':
|
|
30
|
-
return (
|
|
31
|
-
<TextField.Root
|
|
32
|
-
size="1"
|
|
33
|
-
type="number"
|
|
34
|
-
value={value?.toString() || ''}
|
|
35
|
-
onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
|
|
36
|
-
placeholder={field.description || '请输入数字'}
|
|
37
|
-
min={field.min}
|
|
38
|
-
max={field.max}
|
|
39
|
-
/>
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
case 'boolean':
|
|
43
|
-
return (
|
|
44
|
-
<Flex align="center" gap="2">
|
|
45
|
-
<Switch
|
|
46
|
-
checked={value === true}
|
|
47
|
-
onCheckedChange={onChange}
|
|
48
|
-
/>
|
|
49
|
-
<Text size="2" color={value ? 'green' : 'gray'}>
|
|
50
|
-
{value ? '已启用' : '已禁用'}
|
|
51
|
-
</Text>
|
|
52
|
-
</Flex>
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
case 'object': {
|
|
56
|
-
const objectFields = field.dict || field.properties || {}
|
|
57
|
-
return (
|
|
58
|
-
<Card size="1">
|
|
59
|
-
<Flex direction="column" gap="2" p="2">
|
|
60
|
-
{Object.entries(objectFields).map(([key, nestedField]: [string, any]) => (
|
|
61
|
-
<Box key={key}>
|
|
62
|
-
<Text size="1" weight="bold">{key}</Text>
|
|
63
|
-
<NestedFieldRenderer
|
|
64
|
-
fieldName={key}
|
|
65
|
-
field={nestedField}
|
|
66
|
-
value={value?.[key]}
|
|
67
|
-
onChange={(val) => {
|
|
68
|
-
onChange({ ...value, [key]: val })
|
|
69
|
-
}}
|
|
70
|
-
/>
|
|
71
|
-
</Box>
|
|
72
|
-
))}
|
|
73
|
-
</Flex>
|
|
74
|
-
</Card>
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
default:
|
|
79
|
-
return (
|
|
80
|
-
<TextArea
|
|
81
|
-
size="1"
|
|
82
|
-
value={typeof value === 'object' ? JSON.stringify(value, null, 2) : value || ''}
|
|
83
|
-
onChange={(e) => {
|
|
84
|
-
try {
|
|
85
|
-
onChange(JSON.parse(e.target.value))
|
|
86
|
-
} catch {
|
|
87
|
-
onChange(e.target.value)
|
|
88
|
-
}
|
|
89
|
-
}}
|
|
90
|
-
rows={3}
|
|
91
|
-
className="font-mono text-xs"
|
|
92
|
-
/>
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
@@ -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
|
-
|