@zhin.js/client 1.0.3 → 1.0.4

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.
@@ -0,0 +1,105 @@
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
+ }
@@ -0,0 +1,110 @@
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
+ }
@@ -0,0 +1,95 @@
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
+ }
@@ -0,0 +1,237 @@
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
+ }
@@ -0,0 +1,46 @@
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
+ }
@@ -2,8 +2,9 @@ import { useEffect, useState } from 'react'
2
2
  import { useParams, useNavigate } from 'react-router'
3
3
  import * as Themes from '@radix-ui/themes'
4
4
  import { Icons } from '@zhin.js/client'
5
+ import {PluginConfigForm} from '../components/PluginConfigForm'
5
6
 
6
- const { Flex, Box, Spinner, Text, Callout, Heading, Badge, Grid, Card, Button, Code, Separator, ScrollArea } = Themes
7
+ const { Flex, Box, Spinner, Text, Callout, Heading, Badge, Grid, Card, Button, Code, Separator, ScrollArea, Dialog } = Themes
7
8
 
8
9
  interface PluginDetail {
9
10
  name: string
@@ -31,7 +32,7 @@ interface PluginDetail {
31
32
  pattern: string
32
33
  running: boolean
33
34
  }>
34
- schemas: Array<{
35
+ definitions: Array<{
35
36
  name: string
36
37
  fields: string[]
37
38
  }>
@@ -41,7 +42,7 @@ interface PluginDetail {
41
42
  middlewareCount: number
42
43
  contextCount: number
43
44
  cronCount: number
44
- schemaCount: number
45
+ definitionCount: number
45
46
  }
46
47
  }
47
48
 
@@ -77,6 +78,8 @@ export default function DashboardPluginDetail() {
77
78
  }
78
79
  }
79
80
 
81
+
82
+
80
83
  if (loading) {
81
84
  return (
82
85
  <Flex align="center" justify="center" className="h-full">
@@ -111,7 +114,7 @@ export default function DashboardPluginDetail() {
111
114
  <Box>
112
115
  {/* 头部 */}
113
116
  <Flex direction="column" gap="3" mb="4">
114
- <Button variant="ghost" onClick={() => navigate('/plugins')} size="2" className="self-start">
117
+ <Button variant="ghost" onClick={() => navigate('/plugins')} size="2">
115
118
  <Icons.ArrowLeft className="w-4 h-4" />
116
119
  返回
117
120
  </Button>
@@ -132,7 +135,15 @@ export default function DashboardPluginDetail() {
132
135
  </Flex>
133
136
  </Flex>
134
137
 
135
- <Separator size="4" mb="4" />
138
+ {/* 插件配置折叠面板 */}
139
+ <PluginConfigForm
140
+ pluginName={plugin.name}
141
+ onSuccess={() => {
142
+ // 配置更新会通过 WebSocket 自动同步
143
+ }}
144
+ />
145
+
146
+ <Separator size="4" my="4" />
136
147
 
137
148
  {/* 统计概览 - 紧凑卡片 */}
138
149
  <Grid columns={{ initial: '2', sm: '3', md: '6' }} gap="2" mb="4">
@@ -179,7 +190,7 @@ export default function DashboardPluginDetail() {
179
190
  <Card size="1">
180
191
  <Flex direction="column" align="center" gap="1" p="2">
181
192
  <Icons.FileText className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
182
- <Text size="4" weight="bold">{plugin.statistics.schemaCount}</Text>
193
+ <Text size="4" weight="bold">{plugin.statistics.definitionCount}</Text>
183
194
  <Text size="1" color="gray">数据模型</Text>
184
195
  </Flex>
185
196
  </Card>
@@ -319,22 +330,22 @@ export default function DashboardPluginDetail() {
319
330
  )}
320
331
 
321
332
  {/* 数据模型列表 */}
322
- {plugin.schemas.length > 0 && (
333
+ {plugin.definitions.length > 0 && (
323
334
  <Card size="2">
324
335
  <Flex direction="column" gap="2" p="3">
325
336
  <Flex align="center" gap="2">
326
337
  <Icons.FileText className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
327
338
  <Heading size="3">数据模型</Heading>
328
- <Badge size="1">{plugin.schemas.length}</Badge>
339
+ <Badge size="1">{plugin.definitions.length}</Badge>
329
340
  </Flex>
330
341
  <Separator size="4" />
331
342
  <Flex direction="column" gap="2" className="max-h-60 overflow-y-auto">
332
- {plugin.schemas.map((schema, index) => (
343
+ {plugin.definitions.map((definition, index) => (
333
344
  <Box key={index} className="rounded-lg bg-gray-50 dark:bg-gray-900 p-2">
334
345
  <Flex direction="column" gap="1">
335
- <Code size="2">{schema.name}</Code>
346
+ <Code size="2">{definition.name}</Code>
336
347
  <Text size="1" color="gray">
337
- 字段: {schema.fields.join(', ')}
348
+ 字段: {definition.fields.join(', ')}
338
349
  </Text>
339
350
  </Flex>
340
351
  </Box>