import React, { useCallback, useMemo } from 'react'; import { Form, Input, InputNumber, Switch, Select, DatePicker, Button, message, Divider, Typography, Tooltip, } from 'antd'; import { QuestionCircleOutlined, SaveOutlined } from '@ant-design/icons'; import type { PluginSettingField, PluginSettingType, } from '../api/plugins'; const { Text } = Typography; interface PluginSettingsFormProps { /** manifest 中声明的 settings 字段 */ fields: PluginSettingField[]; /** 当前存储的配置值 */ values: Record; /** 插件版本(乐观锁) */ recordVersion: number; /** 保存回调 */ onSave: (config: Record, version: number) => Promise; /** 是否只读 */ readOnly?: boolean; } /** 根据 manifest settings 声明自动渲染配置表单 */ const PluginSettingsForm: React.FC = ({ fields, values, recordVersion, onSave, readOnly = false, }) => { const [form] = Form.useForm(); const [saving, setSaving] = React.useState(false); const initialValues = useMemo(() => { const merged: Record = {}; for (const f of fields) { merged[f.name] = values[f.name] ?? f.default_value ?? getDefaultForType(f.field_type); } return merged; }, [fields, values]); const handleSave = useCallback(async () => { try { const formValues = await form.validateFields(); setSaving(true); await onSave(formValues, recordVersion); message.success('配置已保存'); } catch (err: unknown) { if (err && typeof err === 'object' && 'errorFields' in err) { // antd 表单校验错误,无需额外提示 return; } message.error(err instanceof Error ? err.message : '保存失败'); } finally { setSaving(false); } }, [form, onSave, recordVersion]); const grouped = useMemo(() => { const groups = new Map(); for (const f of fields) { const group = f.group ?? ''; const list = groups.get(group) ?? []; list.push(f); groups.set(group, list); } return groups; }, [fields]); const renderField = (field: PluginSettingField) => { const label = ( {field.display_name} {field.description && ( )} ); const rules: Array<{ required: boolean; message?: string; type?: 'string' | 'number' | 'boolean' | 'url' | 'email' }> = []; if (field.required) { rules.push({ required: true, message: `请输入${field.display_name}` }); } const widget = renderWidget(field, readOnly); return ( {widget} ); }; const groupEntries = Array.from(grouped.entries()); return (
{groupEntries.map(([group, groupFields], gi) => ( {group ? ( {group} ) : null} {groupFields.map(renderField)} ))} {!readOnly && ( )}
); }; function renderWidget(field: PluginSettingField, readOnly: boolean): React.ReactNode { switch (field.field_type) { case 'text': return ; case 'number': { const props: Record = { disabled: readOnly, placeholder: `请输入${field.display_name}`, style: { width: '100%' }, }; if (field.range) { props.min = field.range[0]; props.max = field.range[1]; } return ; } case 'boolean': return ; case 'select': return ( { if (typeof o === 'object' && o !== null && 'label' in o && 'value' in o) { return o as { label: string; value: string }; } return { label: String(o), value: String(o) }; })} /> ); case 'color': return ; case 'date': return ; case 'datetime': return ; case 'json': return ; default: return ; } } function getDefaultForType(type: PluginSettingType): unknown { switch (type) { case 'text': case 'color': return ''; case 'number': return 0; case 'boolean': return false; case 'select': return undefined; case 'multiselect': return []; case 'date': case 'datetime': return undefined; case 'json': return ''; default: return ''; } } export default PluginSettingsForm;