// ============================================================ // ModelInputForm — 模型输入表单(纯布局外壳) // // 布局:Header(模型信息+meta) → Body(fields.map 渲染) → Footer(价格+提交) // // 与旧版的区别: // - 控件渲染逻辑已提取到 useUI/ 组件体系 // - param_schema 解析已提取到 useModelUI Hook // - 本组件只负责:布局、Form 容器、提交逻辑 // ============================================================ import { useEffect, useMemo, useCallback } from 'react'; import { Form, Button, Typography, Empty, Tag, Space, theme as antTheme, message, } from 'antd'; import { SendOutlined, ClockCircleOutlined } from '@ant-design/icons'; import type { UploadFile } from 'antd/es/upload'; import { useHomeContext } from '@/pages/home/HomeContent'; import { useAppContext } from '@/components/AppProvider'; import { useSubmitTask } from '@/hooks/use-api'; import { emit, EVENTS } from '@/utils/event-bus'; import { OutputTypeMap } from '@/services/modules/task'; import type { OutputTypeString, TaskInfoItemResponseBody } from '@/services/modules/task'; import { useModelUI, type UIFieldConfig } from './useModelUI'; const { Text, Title } = Typography; // ---------- 组件 ---------- export function ModelInputForm() { const { token } = antTheme.useToken(); const { isLoggedIn } = useAppContext(); const { selectedModel } = useHomeContext(); const [form] = Form.useForm(); // 提交突变 Hook const { execute: submitTask, loading: submitting, errorMessage } = useSubmitTask({ onSuccess: useCallback((_data: TaskInfoItemResponseBody) => { message.success('任务已提交'); emit(EVENTS.TASK_SUBMITTED); }, []), }); // ======== useModelUI:param_schema → UIFieldConfig[] ======== const fields: UIFieldConfig[] = useModelUI(selectedModel?.param_schema || []); // 构建初始值 const initialValues = useMemo(() => { const iv: Record = {}; fields.forEach((f) => { if (f.defaultValue !== undefined && f.defaultValue !== null) { iv[f.name] = f.defaultValue; } }); return iv; }, [fields]); // 选中模型时重置表单 useEffect(() => { if (selectedModel) { form.resetFields(); } }, [selectedModel?.id]); // eslint-disable-line react-hooks/exhaustive-deps // ======== 提交 ======== const handleSubmit = async () => { if (!isLoggedIn) { message.warning('请先登录后再提交任务'); return; } if (!selectedModel) return; try { const values = await form.validateFields(); // 将表单值全部转为字符串(CreatTaskRequestBody.params 为 Record) const params: Record = {}; for (const field of fields) { const value = values[field.name]; if (value === undefined || value === null) continue; // 文件上传 → 提取 URL 数组 → JSON 字符串 if (field.isFileWidget) { const fileList = value as UploadFile[]; const urls = fileList .filter((f) => f.status === 'done') .map((f) => f.response?.url || f.url || f.name) .filter(Boolean) as string[]; if (urls.length > 0) { params[field.name] = urls.length === 1 ? urls[0] : JSON.stringify(urls); } continue; } // 布尔值 → "true"/"false" if (typeof value === 'boolean') { params[field.name] = String(value); continue; } // 数值 / 字符串 → String params[field.name] = String(value); } await submitTask({ model_id: selectedModel.id, params }); } catch (err) { if (err && typeof err === 'object' && 'errorFields' in err) { return; // 表单验证错误,antd 自动提示 } message.error(errorMessage || '任务提交失败,请重试'); } }; // ======== 未选择模型 ======== if (!selectedModel) { return (
); } // ======== 提取 meta 信息 ======== const metaConfig = (selectedModel.meta_config || {}) as Record; const uiConfig = (selectedModel.ui_config || {}) as Record; const pricingConfig = (selectedModel.pricing_config || {}) as Record; const outputType = metaConfig.output_type as OutputTypeString | undefined; const estimatedDuration = metaConfig.estimated_duration_seconds as number | undefined; const badge = uiConfig.badge as string | undefined; const group = uiConfig.group as string | undefined; const helpUrl = metaConfig.help_url as string | undefined; const priceUnit = (pricingConfig.unit as Record)?.name as string | undefined; const price = pricingConfig.price as number | undefined; const priceType = pricingConfig.price_type as string | undefined; // ======== 正常渲染 ======== return (
{/* ======== Header:模型信息 ======== */}
{badge && {badge}} {selectedModel.name}
{selectedModel.provider_name} {group ? ` · ${group}` : selectedModel.category_name ? ` · ${selectedModel.category_name}` : ''}
{outputType && ( {OutputTypeMap[outputType] || outputType} )} {estimatedDuration !== undefined && ( } color="default" style={{ fontSize: 11, lineHeight: '18px' }} > 约 {estimatedDuration} 秒 )} {helpUrl && ( 使用帮助 ↗ )}
{/* ======== Body:参数列表(useModelUI 驱动) ======== */}
{fields.length === 0 ? ( ) : (
{fields.map((field) => ( {field.label} {field.tips && ( {field.tips.length > 60 ? `${field.tips.slice(0, 60)}...` : field.tips} )} } rules={ field.must ? [{ required: true, message: `请输入${field.label}` }] : undefined } valuePropName={field.valuePropName} // 文件控件:从 Upload onChange 事件中提取 fileList {...(field.isFileWidget ? { getValueFromEvent: (e: { fileList: UploadFile[] }) => e.fileList } : {})} > ))}
)}
{/* ======== Footer:价格 + 提交 ======== */}
{price !== undefined && priceType === 'SIMPLE' && ( ¥{price}/{priceUnit || '次'} )} {priceType === 'MULTI_DIMENSION' && ( 按规格计费 )} {priceType === 'BILLING_ADAPTER' && ( 按用量计费 )}
); }