import React, { useEffect, useMemo, useRef, useState } from 'react'; import { XCard, registerCatalog, type ActionPayload, type XAgentCommand_v0_9, } from '@ant-design/x-card'; import { Alert, Button, DatePicker, Flex, Form, Input, InputNumber, Select, Typography, } from 'antd'; import dayjs from 'dayjs'; import type { AiFormField, AiFormSchema } from './types'; const FORM_CATALOG_ID = 'gongxue-form-catalog'; registerCatalog({ catalogId: FORM_CATALOG_ID, components: { FormPreview: { type: 'object', properties: { form: { type: 'object' }, disabled: { type: 'boolean' }, }, }, }, }); function surfaceId(formId: string): string { return `form-${formId}`; } function initialValue(field: AiFormField): unknown { if (field.type === 'date' && typeof field.defaultValue === 'string') { const parsed = dayjs(field.defaultValue); return parsed.isValid() ? parsed : undefined; } return field.defaultValue; } function normalizeValues( fields: AiFormField[], raw: Record, ): Record { const values: Record = {}; for (const field of fields) { const value = raw[field.name]; if (value === undefined || value === null || value === '') continue; values[field.name] = field.type === 'date' && dayjs.isDayjs(value) ? value.format('YYYY-MM-DD') : value; } return values; } interface FormPreviewProps { form?: AiFormSchema & { submitting?: boolean; submitted?: boolean; error?: string | null; }; disabled?: boolean; onAction?: (name: string, context: Record) => void; } /** * A2UI component registered for the `gongxue-form-catalog` catalog. * Receives the validated form schema through data binding and reports * normalized values back through the `form:submit` action. */ const FormPreview: React.FC = ({ form, disabled, onAction }) => { const submitting = Boolean(form?.submitting); const initialValues = useMemo( () => Object.fromEntries((form?.fields ?? []).map((field) => [field.name, initialValue(field)])), [form?.fields], ); if (!form) return null; const finished = Boolean(form.submitted) || form.status === 'submitted'; const handleFinish = (values: Record) => { onAction?.('form:submit', { values: normalizeValues(form.fields, values) }); }; return ( {form.title} {form.description && ( {form.description} )} {finished ? ( ) : (
void handleFinish(values as Record)} disabled={disabled || submitting} requiredMark={false} > {form.fields.map((field) => ( {field.type === 'textarea' ? ( ) : field.type === 'number' ? ( ) : field.type === 'select' ? ( )} ))} {form.error && ( )} )}
); }; export interface DynamicFormProps { form: AiFormSchema; disabled?: boolean; onSubmit: (values: Record) => void | Promise; } /** * A2UI form rendered through the official @ant-design/x-card renderer. * The validated schema is bound into the surface data model; submit * success/failure/loading transitions are pushed as incremental commands. */ export const DynamicForm: React.FC = ({ form, disabled, onSubmit }) => { const [submitting, setSubmitting] = useState(false); const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(null); const commandsRef = useRef([]); const [commands, setCommands] = useState([]); const idRef = useRef(''); useEffect(() => { const sid = surfaceId(form.id); if (idRef.current !== sid) { commandsRef.current = []; idRef.current = sid; } const cmds = commandsRef.current; if (cmds.length === 0) { cmds.push({ version: 'v0.9', createSurface: { surfaceId: sid, catalogId: FORM_CATALOG_ID }, }); } cmds.push({ version: 'v0.9', updateDataModel: { surfaceId: sid, path: '/form', value: { ...form, submitting, submitted, error }, }, }); cmds.push({ version: 'v0.9', updateComponents: { surfaceId: sid, components: [ { id: 'root', component: 'FormPreview', form: { path: '/form' }, disabled: Boolean(disabled), }, ], }, }); setCommands([...cmds]); }, [disabled, error, form, submitted, submitting]); const handleSubmit = async (values: Record) => { if (submitting) return; setSubmitting(true); setError(null); try { await onSubmit(values); setSubmitted(true); } catch (reason) { setError(reason instanceof Error ? reason.message : '提交失败,请稍后重试'); } finally { setSubmitting(false); } }; const handleAction = (payload: ActionPayload) => { if (payload.name !== 'form:submit') return; const values = payload.context?.values && typeof payload.context.values === 'object' ? (payload.context.values as Record) : {}; void handleSubmit(values); }; return (
); };