import * as React from 'react' import * as LucideIcons from 'lucide-react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle, } from '@/components/ui/card' import { Separator } from '@/components/ui/separator' import { fieldHooks, type FieldHookRegistry } from '@/lib/field-hooks' import type { ConfigSchema, FieldSchema } from '@/types/config-schema' import { DynamicField } from './DynamicField' export interface DynamicConfigFormProps { schema: ConfigSchema values: Record onChange: (field: string, value: unknown) => void basePath?: string hooks?: FieldHookRegistry /** 嵌套层级:0 = tab 内容层,1 = section 内容层,2+ = 更深嵌套 */ level?: number advancedVisible?: boolean sectionColumns?: 1 | 2 } function buildFieldPath(basePath: string, fieldName: string) { return basePath ? `${basePath}.${fieldName}` : fieldName } function resolveSectionTitle(schema: ConfigSchema) { return schema.uiLabel || schema.classDoc || schema.className } function SectionIcon({ iconName }: { iconName?: string }) { if (!iconName) return null const IconComponent = LucideIcons[iconName as keyof typeof LucideIcons] as | React.ComponentType<{ className?: string }> | undefined if (!IconComponent) return null return } export function AdvancedSettingsButton({ active, onClick, }: { active: boolean onClick: () => void }) { return ( ) } function DynamicConfigSection({ advancedVisible, basePath, hooks, level, nestedSchema, onChange, sectionKey, sectionTitle, values, }: { advancedVisible: boolean basePath: string hooks: FieldHookRegistry level: number nestedSchema: ConfigSchema onChange: (field: string, value: unknown) => void sectionKey: string sectionTitle: string values: Record }) { return (
{sectionTitle}
onChange(`${sectionKey}.${field}`, value)} basePath={basePath} hooks={hooks} level={level} advancedVisible={advancedVisible} sectionColumns={1} />
) } /** * DynamicConfigForm - 动态配置表单组件 * * 根据 ConfigSchema 渲染表单字段,支持: * 1. Hook 系统:通过 FieldHookRegistry 自定义字段渲染 * - replace 模式:完全替换默认渲染 * - wrapper 模式:包装默认渲染(通过 children 传递) * 2. 嵌套 schema:递归渲染 schema.nested 中的子配置 * 3. 高级设置:由栏目标题右侧按钮控制显示 */ export const DynamicConfigForm: React.FC = ({ schema, values, onChange, basePath = '', hooks = fieldHooks, level = 0, advancedVisible, sectionColumns = 1, }) => { const resolvedAdvancedVisible = advancedVisible ?? false const fieldMap = React.useMemo( () => new Map(schema.fields.map((field) => [field.name, field])), [schema.fields], ) const renderField = (field: FieldSchema) => { const fieldPath = buildFieldPath(basePath, field.name) const nestedSchema = schema.nested?.[field.name] if (hooks.has(fieldPath)) { const hookEntry = hooks.get(fieldPath) if (!hookEntry) return null if (hookEntry.type === 'hidden') return null const HookComponent = hookEntry.component if (hookEntry.type === 'replace') { return ( onChange(field.name, v)} onParentChange={onChange} schema={field} nestedSchema={nestedSchema} parentValues={values} /> ) } return ( onChange(field.name, v)} onParentChange={onChange} schema={field} nestedSchema={nestedSchema} parentValues={values} > onChange(field.name, v)} fieldPath={fieldPath} /> ) } return ( onChange(field.name, v)} fieldPath={fieldPath} /> ) } const shouldRenderFieldInline = (field: FieldSchema) => { const fieldPath = buildFieldPath(basePath, field.name) if (hooks.get(fieldPath)?.type === 'hidden') { return false } if (!schema.nested?.[field.name]) { return true } return hooks.get(fieldPath)?.type === 'replace' } const schemaHasVisibleContent = React.useCallback( (targetSchema: ConfigSchema, targetBasePath: string): boolean => { const targetFields = targetSchema.fields ?? [] const hasVisibleInlineField = targetFields.some((field) => { const fieldPath = buildFieldPath(targetBasePath, field.name) const hookEntry = hooks.get(fieldPath) if (hookEntry?.type === 'hidden') { return false } if (targetSchema.nested?.[field.name] && hookEntry?.type !== 'replace') { return false } return resolvedAdvancedVisible || !field.advanced }) if (hasVisibleInlineField) { return true } return Object.entries(targetSchema.nested ?? {}).some(([key, nestedSchema]) => { const nestedField = targetFields.find((field) => field.name === key) const nestedFieldPath = buildFieldPath(targetBasePath, key) const hookEntry = hooks.get(nestedFieldPath) if (hookEntry?.type === 'hidden') { return false } if (nestedField?.advanced && !resolvedAdvancedVisible) { return false } if (hookEntry?.type === 'replace') { return true } return schemaHasVisibleContent(nestedSchema, nestedFieldPath) }) }, [hooks, resolvedAdvancedVisible], ) const inlineFields = schema.fields.filter(shouldRenderFieldInline) const inlineNestedFieldNames = new Set( inlineFields .filter((field) => Boolean(schema.nested?.[field.name])) .map((field) => field.name), ) const normalFields = inlineFields.filter((field) => !field.advanced) const advancedFields = inlineFields.filter((field) => field.advanced) const visibleFields = resolvedAdvancedVisible ? [...normalFields, ...advancedFields] : normalFields const groupFieldsByRow = (fields: FieldSchema[]) => { const rows: FieldSchema[][] = [] let currentRow: FieldSchema[] = [] let currentRowKey: string | undefined for (const field of fields) { const rowKey = field['x-row'] if (rowKey && rowKey === currentRowKey) { currentRow.push(field) continue } if (currentRow.length > 0) { rows.push(currentRow) } currentRow = [field] currentRowKey = rowKey } if (currentRow.length > 0) { rows.push(currentRow) } return rows } const renderRows = (rows: FieldSchema[][]) => ( <> {rows.map((row) => ( row.length > 1 ? (
field.name).join('|')} className="grid gap-4 py-1 md:grid-cols-[repeat(var(--field-row-count),minmax(0,1fr))]" style={{ '--field-row-count': row.length } as React.CSSProperties} > {row.map((field) => (
{renderField(field)}
))}
) : (
{renderField(row[0])}
) ))} ) const renderFieldList = (fields: FieldSchema[]) => ( <> {groupFieldsByRow(fields).map((row, index) => ( field.name).join('|')}> {index > 0 && } {renderRows([row])} ))} ) return (
{visibleFields.length > 0 && (
{renderFieldList(visibleFields)}
)} {schema.nested && (() => { const nestedSections = Object.entries(schema.nested) .filter(([key]) => !inlineNestedFieldNames.has(key)) .map(([key, nestedSchema]) => { const nestedField = fieldMap.get(key) const nestedFieldPath = buildFieldPath(basePath, key) if (hooks.has(nestedFieldPath)) { const hookEntry = hooks.get(nestedFieldPath) if (!hookEntry) return null if (hookEntry.type === 'hidden') return null if (nestedField?.advanced && !resolvedAdvancedVisible) return null if ( hookEntry.type !== 'replace' && nestedSchema && !schemaHasVisibleContent(nestedSchema, nestedFieldPath) ) { return null } const HookComponent = hookEntry.component if (hookEntry.type === 'replace') { return (
onChange(key, v)} onParentChange={onChange} schema={nestedField ?? nestedSchema} nestedSchema={nestedSchema} parentValues={values} />
) } return (
onChange(key, v)} onParentChange={onChange} schema={nestedField ?? nestedSchema} nestedSchema={nestedSchema} parentValues={values} > ) || {}} onChange={(field, value) => onChange(`${key}.${field}`, value)} basePath={nestedFieldPath} hooks={hooks} level={level + 1} advancedVisible={resolvedAdvancedVisible} sectionColumns={1} />
) } const sectionTitle = resolveSectionTitle(nestedSchema) if (!schemaHasVisibleContent(nestedSchema, nestedFieldPath)) { return null } if (level === 0) { return ( ) || {}} onChange={onChange} basePath={nestedFieldPath} hooks={hooks} level={level + 1} sectionKey={key} sectionTitle={sectionTitle} /> ) } return (
{sectionTitle}
) || {}} onChange={(field, value) => onChange(`${key}.${field}`, value)} basePath={nestedFieldPath} hooks={hooks} level={level + 1} advancedVisible={resolvedAdvancedVisible} sectionColumns={1} />
) }) const visibleNestedSections = nestedSections.filter( (section): section is React.ReactElement => Boolean(section), ) if (level === 0 && sectionColumns === 2 && visibleNestedSections.length > 1) { return (
{visibleNestedSections}
) } return visibleNestedSections })()}
) }