feat(dashboard): create DynamicConfigForm component with Hook integration

- Accept schema, values, onChange, hooks props
- Check for field hooks, use if present (replace/wrapper modes)
- Fall back to DynamicField for non-hooked fields
- Support nested schema rendering with recursive calls
- Export DynamicConfigForm and DynamicField from index

Groups with Task 5. Completes Task 6 of webui-config-visualization-refactor plan.
pull/1496/head
DrSmoothl 2026-02-17 17:20:50 +08:00
parent 4e5d091417
commit 449e2f470c
No known key found for this signature in database
2 changed files with 116 additions and 0 deletions

View File

@ -0,0 +1,114 @@
import * as React from 'react'
import type { ConfigSchema, FieldSchema } from '@/types/config-schema'
import { fieldHooks, type FieldHookRegistry } from '@/lib/field-hooks'
import { DynamicField } from './DynamicField'
export interface DynamicConfigFormProps {
schema: ConfigSchema
values: Record<string, unknown>
onChange: (field: string, value: unknown) => void
hooks?: FieldHookRegistry
}
/**
* DynamicConfigForm -
*
* ConfigSchema
* 1. Hook FieldHookRegistry
* - replace
* - wrapper children
* 2. schema schema.nested
* 3. 使 DynamicField
*/
export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
schema,
values,
onChange,
hooks = fieldHooks, // 默认使用全局单例
}) => {
/**
*
* Hook Hook
*/
const renderField = (field: FieldSchema) => {
const fieldPath = field.name
// 检查是否有注册的 Hook
if (hooks.has(fieldPath)) {
const hookEntry = hooks.get(fieldPath)
if (!hookEntry) return null // Type guard理论上不会发生
const HookComponent = hookEntry.component
if (hookEntry.type === 'replace') {
// replace 模式:完全替换默认渲染
return (
<HookComponent
fieldPath={fieldPath}
value={values[field.name]}
onChange={(v) => onChange(field.name, v)}
/>
)
} else {
// wrapper 模式:包装默认渲染
return (
<HookComponent
fieldPath={fieldPath}
value={values[field.name]}
onChange={(v) => onChange(field.name, v)}
>
<DynamicField
schema={field}
value={values[field.name]}
onChange={(v) => onChange(field.name, v)}
fieldPath={fieldPath}
/>
</HookComponent>
)
}
}
// 无 Hook使用默认渲染
return (
<DynamicField
schema={field}
value={values[field.name]}
onChange={(v) => onChange(field.name, v)}
fieldPath={fieldPath}
/>
)
}
return (
<div className="space-y-4">
{/* 渲染顶层字段 */}
{schema.fields.map((field) => (
<div key={field.name}>{renderField(field)}</div>
))}
{/* 渲染嵌套 schema */}
{schema.nested &&
Object.entries(schema.nested).map(([key, nestedSchema]) => (
<div key={key} className="mt-6 space-y-4">
{/* 嵌套 schema 标题 */}
<div className="border-b pb-2">
<h3 className="text-lg font-semibold">{nestedSchema.className}</h3>
{nestedSchema.classDoc && (
<p className="text-sm text-muted-foreground">{nestedSchema.classDoc}</p>
)}
</div>
{/* 递归渲染嵌套表单 */}
<DynamicConfigForm
schema={nestedSchema}
values={(values[key] as Record<string, unknown>) || {}}
onChange={(field, value) => onChange(`${key}.${field}`, value)}
hooks={hooks}
/>
</div>
))}
</div>
)
}

View File

@ -0,0 +1,2 @@
export { DynamicConfigForm } from './DynamicConfigForm'
export { DynamicField } from './DynamicField'