import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Alert, Button, Card, Col, Collapse, Divider, Form, Input, InputNumber, Modal, Popconfirm, Row, Select, Space, Spin, Switch, Tabs, Tag, Typography, message, } from 'antd'; import { DeleteOutlined, DownloadOutlined, EyeOutlined, PlusOutlined, ReloadOutlined, SaveOutlined, UploadOutlined, } from '@ant-design/icons'; import { exportVideoPromptSchemaConfig, getVideoPromptSchemaConfig, importVideoPromptSchemaConfig, previewVideoPromptSchemaConfig, resetVideoPromptSchemaConfig, saveVideoPromptSchemaConfig, } from '../api'; import type { VideoPromptFlowItemField, VideoPromptSchemaConfigData, VideoPromptSchemaNode, VideoPromptSchemaNodeType, VideoPromptTimePlanRule, } from '../types'; const { Title, Text, Paragraph } = Typography; const { TextArea } = Input; const NODE_TYPES: { label: string; value: VideoPromptSchemaNodeType }[] = [ { label: '对象', value: 'object' }, { label: '数组', value: 'array' }, { label: '字符串', value: 'string' }, { label: '数字', value: 'number' }, { label: '布尔', value: 'boolean' }, { label: '流程数组', value: 'flow' }, ]; const VIDEO_SPEC_SECTION_KEY = '画面属性'; const VIDEO_SPEC_LOCKED_FIELDS = new Set(['视频时长', '视频比例', '清晰度', '帧率', '推荐分辨率']); function isLockedVideoSpecField(sectionKey: string | undefined, fieldKey: string | undefined): boolean { return sectionKey === VIDEO_SPEC_SECTION_KEY && !!fieldKey && VIDEO_SPEC_LOCKED_FIELDS.has(fieldKey); } function isLockedVideoSpecSection(sectionKey: string | undefined): boolean { return sectionKey === VIDEO_SPEC_SECTION_KEY; } function clone(value: T): T { return JSON.parse(JSON.stringify(value ?? null)); } function pick(obj: any, camelKey: string, snakeKey: string, fallback?: T): T { if (!obj || typeof obj !== 'object') return fallback as T; if (obj[camelKey] !== undefined) return obj[camelKey] as T; if (obj[snakeKey] !== undefined) return obj[snakeKey] as T; return fallback as T; } function normalizeFlowField(field: any): VideoPromptFlowItemField { return { key: String(field?.key || field?.label || '新流程字段'), label: String(field?.label || field?.key || '新流程字段'), enabled: field?.enabled ?? true, editable: field?.editable ?? true, maxLength: Number(pick(field, 'maxLength', 'max_length', 800) || 800), value: field?.value ?? '', }; } function normalizeNode(node: any): VideoPromptSchemaNode { const type = String(node?.type || 'string') as VideoPromptSchemaNodeType; const normalized: VideoPromptSchemaNode = { key: String(node?.key || node?.label || '新字段'), label: String(node?.label || node?.key || '新字段'), type, enabled: node?.enabled ?? true, editable: node?.editable ?? true, maxLength: Number(pick(node, 'maxLength', 'max_length', type === 'flow' ? 800 : 1000) || (type === 'flow' ? 800 : 1000)), }; if (type === 'object') { normalized.children = Array.isArray(node?.children) ? node.children.map(normalizeNode) : []; } else if (type === 'flow') { normalized.contentKey = String(pick(node, 'contentKey', 'content_key', node?.key === '镜头流程' ? '镜头内容' : '动作内容')); const aliases = pick(node, 'contentAliases', 'content_aliases', []); normalized.contentAliases = Array.isArray(aliases) ? aliases.map(item => String(item)).filter(Boolean) : []; const itemFields = pick(node, 'itemFields', 'item_fields', []); normalized.itemFields = Array.isArray(itemFields) && itemFields.length > 0 ? itemFields.map(normalizeFlowField) : [{ key: normalized.contentKey, label: normalized.contentKey, enabled: true, editable: true, maxLength: normalized.maxLength || 800, value: '' }]; } else { normalized.value = node?.value ?? (type === 'array' ? [] : type === 'boolean' ? false : type === 'number' ? 0 : '无'); } return normalized; } function normalizeTimeRule(rule: any): VideoPromptTimePlanRule { const rawSegments = Array.isArray(rule?.segments) ? rule.segments : []; const rawRatios = Array.isArray(rule?.ratios) ? rule.ratios : []; return { minDuration: Number(pick(rule, 'minDuration', 'min_duration', 1) || 1), maxDuration: Number(pick(rule, 'maxDuration', 'max_duration', 5) || 5), ratios: rawRatios.map((item: any) => Number(item || 0)), segments: rawSegments.map((segment: any) => ({ stage: String(segment?.stage || '新阶段'), description: String(segment?.description || segment?.desc || '阶段说明'), })), }; } function normalizeConfig(data: any): VideoPromptSchemaConfigData { const rawSections = Array.isArray(data?.sections) ? data.sections : []; const rawRules = Array.isArray(data?.timePlanRules) ? data.timePlanRules : Array.isArray(data?.time_plan_rules) ? data.time_plan_rules : []; return { version: data?.version || 'video_prompt_schema_config_v1', enabled: data?.enabled ?? true, sections: rawSections.map(normalizeNode), timePlanRules: rawRules.map(normalizeTimeRule), }; } function toApiNode(node: VideoPromptSchemaNode, parentKey?: string): any { const lockedSection = isLockedVideoSpecSection(node.key); const lockedField = isLockedVideoSpecField(parentKey, node.key); const base: any = { key: node.key, label: node.label, type: lockedField ? 'string' : node.type, enabled: lockedSection || lockedField ? true : !!node.enabled, editable: lockedField ? false : !!node.editable, max_length: node.maxLength, }; if (node.type === 'object') { base.children = (node.children || []).map(child => toApiNode(child, node.key)); } else if (node.type === 'flow') { base.content_key = node.contentKey; base.content_aliases = node.contentAliases || []; base.item_fields = (node.itemFields || []).map(field => ({ key: field.key, label: field.label, enabled: !!field.enabled, editable: !!field.editable, max_length: field.maxLength, value: field.value ?? '', })); } else { base.value = node.value; } return base; } function toApiConfig(config: VideoPromptSchemaConfigData, enabled: boolean): any { return { version: config.version, enabled, sections: (config.sections || []).map(section => toApiNode(section)), time_plan_rules: (config.timePlanRules || []).map(rule => ({ min_duration: rule.minDuration, max_duration: rule.maxDuration, ratios: rule.ratios || [], segments: (rule.segments || []).map(segment => ({ stage: segment.stage, description: segment.description, })), })), }; } function runtimePreviewSchema(result: any): any { return result?.runtimeSchema || result?.runtime_schema || {}; } function runtimePreviewTimePlan(result: any): any[] { const value = result?.timePlan || result?.time_plan || runtimePreviewSchema(result)?.动态时间规划 || []; return Array.isArray(value) ? value : []; } function runtimePreviewSnapshot(result: any): any { return result?.schemaConfigSnapshot || result?.schema_config_snapshot || {}; } function downloadJson(filename: string, data: any): void { const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } const createNode = (type: VideoPromptSchemaNodeType = 'string'): VideoPromptSchemaNode => ({ key: '新字段', label: '新字段', type, enabled: true, editable: true, maxLength: type === 'flow' ? 800 : 1000, value: type === 'array' ? [] : type === 'boolean' ? false : type === 'number' ? 0 : '无', children: type === 'object' ? [] : undefined, contentKey: type === 'flow' ? '动作内容' : undefined, contentAliases: type === 'flow' ? ['动作内容', '动作', '动作说明', '内容', '说明'] : undefined, itemFields: type === 'flow' ? [{ key: '动作内容', label: '动作内容', enabled: true, editable: true, maxLength: 800, value: '' }] : undefined, }); const createTimeRule = (): VideoPromptTimePlanRule => ({ minDuration: 1, maxDuration: 5, ratios: [0.2, 0.4, 0.4], segments: [ { stage: '开场吸引', description: '快速吸引注意力' }, { stage: '核心展示', description: '展示主体动作、核心卖点或主要视觉内容' }, { stage: '收尾引导', description: '给出行动引导并稳定落版' }, ], }); const AdminVideoPromptSchemaConfig: React.FC = () => { const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [enabled, setEnabled] = useState(true); const [usingDefault, setUsingDefault] = useState(false); const [config, setConfig] = useState(null); const [defaultConfig, setDefaultConfig] = useState(null); const [previewOpen, setPreviewOpen] = useState(false); const [previewResult, setPreviewResult] = useState(null); const [previewForm] = Form.useForm(); const importInputRef = useRef(null); const sections = config?.sections || []; const timePlanRules = config?.timePlanRules || []; const load = async () => { setLoading(true); try { const res = await getVideoPromptSchemaConfig(); setEnabled(Boolean((res as any).isEnabled ?? (res as any).is_enabled ?? res.data?.enabled ?? false)); setUsingDefault(Boolean((res as any).usingDefault ?? (res as any).using_default ?? false)); setConfig(normalizeConfig(res.data)); setDefaultConfig(normalizeConfig((res as any).defaultData ?? (res as any).default_data)); } catch (error: any) { message.error(error?.message || '加载失败'); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const updateConfig = (updater: (draft: VideoPromptSchemaConfigData) => void) => { setConfig(prev => { const next = normalizeConfig(clone(prev)); updater(next); return next; }); }; const save = async () => { if (!config) return; setSaving(true); try { const res = await saveVideoPromptSchemaConfig({ is_enabled: enabled, data: toApiConfig(config, enabled) }); setConfig(normalizeConfig(res.data)); setEnabled(Boolean((res as any).isEnabled ?? (res as any).is_enabled ?? res.data?.enabled ?? false)); setUsingDefault(Boolean((res as any).usingDefault ?? (res as any).using_default ?? false)); message.success('保存成功'); } catch (error: any) { message.error(error?.message || '保存失败'); } finally { setSaving(false); } }; const resetDefault = async () => { setSaving(true); try { const res = await resetVideoPromptSchemaConfig(); setConfig(normalizeConfig(res.data)); setDefaultConfig(normalizeConfig((res as any).defaultData ?? (res as any).default_data)); setEnabled(Boolean((res as any).isEnabled ?? (res as any).is_enabled ?? res.data?.enabled ?? false)); setUsingDefault(Boolean((res as any).usingDefault ?? (res as any).using_default ?? false)); message.success('已恢复默认配置'); } catch (error: any) { message.error(error?.message || '恢复默认失败'); } finally { setSaving(false); } }; const doExport = async () => { try { const res = await exportVideoPromptSchemaConfig(); downloadJson('video_prompt_schema_config.json', res); } catch (error: any) { message.error(error?.message || '导出失败'); } }; const onImportFile = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ''; if (!file) return; try { const text = await file.text(); const json = JSON.parse(text); const data = json.data || json; const isEnabled = json.isEnabled ?? json.is_enabled ?? enabled; const res = await importVideoPromptSchemaConfig({ is_enabled: !!isEnabled, data }); setConfig(normalizeConfig(res.data)); setEnabled(Boolean((res as any).isEnabled ?? (res as any).is_enabled ?? res.data?.enabled ?? false)); setUsingDefault(Boolean((res as any).usingDefault ?? (res as any).using_default ?? false)); message.success('导入成功'); } catch (error: any) { message.error(error?.message || '导入失败,请检查 JSON 格式'); } }; const openPreview = () => { previewForm.setFieldsValue({ duration: 8, aspectRatio: '9:16', resolution: '1080p', frameRate: '30fps' }); setPreviewResult(null); setPreviewOpen(true); }; const doPreview = async () => { if (!config) return; const values = await previewForm.validateFields(); try { const res = await previewVideoPromptSchemaConfig({ is_enabled: enabled, data: toApiConfig(config, enabled), duration: Number(values.duration || 8), aspect_ratio: values.aspectRatio, resolution: values.resolution, frame_rate: values.frameRate, }); setPreviewResult(res); } catch (error: any) { message.error(error?.message || '预览失败'); } }; const addSection = () => { updateConfig(draft => { draft.sections.push(createNode('object')); }); }; const updateSection = (index: number, patch: Partial) => { updateConfig(draft => { const current = draft.sections[index]; if (isLockedVideoSpecSection(current?.key)) { delete patch.key; delete patch.type; delete patch.enabled; } draft.sections[index] = { ...draft.sections[index], ...patch }; if (patch.type === 'object') { draft.sections[index].children = draft.sections[index].children || []; } if (patch.type === 'flow') { draft.sections[index].contentKey = draft.sections[index].contentKey || '动作内容'; draft.sections[index].contentAliases = draft.sections[index].contentAliases || ['动作内容', '动作', '动作说明', '内容', '说明']; draft.sections[index].itemFields = draft.sections[index].itemFields || [{ key: '动作内容', label: '动作内容', enabled: true, editable: true, maxLength: 800, value: '' }]; } }); }; const deleteSection = (index: number) => { updateConfig(draft => { draft.sections.splice(index, 1); }); }; const addChild = (sectionIndex: number) => { updateConfig(draft => { const section = draft.sections[sectionIndex]; section.children = section.children || []; section.children.push(createNode('string')); }); }; const updateChild = (sectionIndex: number, childIndex: number, patch: Partial) => { updateConfig(draft => { const section = draft.sections[sectionIndex]; const children = section.children || []; const child = children[childIndex]; if (isLockedVideoSpecField(section?.key, child?.key)) { delete patch.key; delete patch.type; delete patch.enabled; patch.editable = false; } children[childIndex] = { ...children[childIndex], ...patch }; draft.sections[sectionIndex].children = children; }); }; const deleteChild = (sectionIndex: number, childIndex: number) => { updateConfig(draft => { draft.sections[sectionIndex].children?.splice(childIndex, 1); }); }; const addFlowField = (sectionIndex: number) => { updateConfig(draft => { const section = draft.sections[sectionIndex]; section.itemFields = section.itemFields || []; section.itemFields.push({ key: '新流程字段', label: '新流程字段', enabled: true, editable: true, maxLength: 800, value: '' }); }); }; const updateFlowField = (sectionIndex: number, fieldIndex: number, patch: Partial) => { updateConfig(draft => { const fields = draft.sections[sectionIndex].itemFields || []; fields[fieldIndex] = { ...fields[fieldIndex], ...patch }; draft.sections[sectionIndex].itemFields = fields; }); }; const deleteFlowField = (sectionIndex: number, fieldIndex: number) => { updateConfig(draft => { draft.sections[sectionIndex].itemFields?.splice(fieldIndex, 1); }); }; const addTimeRule = () => { updateConfig(draft => { draft.timePlanRules.push(createTimeRule()); }); }; const updateTimeRule = (index: number, patch: Partial) => { updateConfig(draft => { draft.timePlanRules[index] = { ...draft.timePlanRules[index], ...patch }; }); }; const deleteTimeRule = (index: number) => { updateConfig(draft => { draft.timePlanRules.splice(index, 1); }); }; const updateSegment = (ruleIndex: number, segmentIndex: number, patch: Partial<{ stage: string; description: string; ratio: number }>) => { updateConfig(draft => { const rule = draft.timePlanRules[ruleIndex]; if (patch.ratio !== undefined) rule.ratios[segmentIndex] = Number(patch.ratio || 0); if (patch.stage !== undefined) rule.segments[segmentIndex].stage = patch.stage; if (patch.description !== undefined) rule.segments[segmentIndex].description = patch.description; }); }; const addSegment = (ruleIndex: number) => { updateConfig(draft => { const rule = draft.timePlanRules[ruleIndex]; rule.ratios.push(1); rule.segments.push({ stage: '新阶段', description: '阶段说明' }); }); }; const deleteSegment = (ruleIndex: number, segmentIndex: number) => { updateConfig(draft => { const rule = draft.timePlanRules[ruleIndex]; rule.ratios.splice(segmentIndex, 1); rule.segments.splice(segmentIndex, 1); }); }; const sectionItems = useMemo(() => sections.map((section, sectionIndex) => ({ key: `section-${sectionIndex}`, label: ( {section.label || section.key || '未命名分组'} {section.key && section.key !== section.label && key: {section.key}} {section.type} {isLockedVideoSpecSection(section.key) && 系统锁定} {!section.enabled && !isLockedVideoSpecSection(section.key) && 已禁用} ), children: ( updateSection(sectionIndex, { key: e.target.value })} /> updateSection(sectionIndex, { label: e.target.value })} /> updateChild(sectionIndex, childIndex, { key: e.target.value })} /> updateChild(sectionIndex, childIndex, { label: e.target.value })} /> updateChild(sectionIndex, childIndex, { value: child.type === 'array' ? e.target.value.split('/').filter(Boolean) : e.target.value })} /> }> updateSection(sectionIndex, { contentKey: e.target.value })} /> updateSection(sectionIndex, { contentAliases: e.target.value.split('/').map(item => item.trim()).filter(Boolean) })} /> {(section.itemFields || []).map((field, fieldIndex) => ( updateFlowField(sectionIndex, fieldIndex, { key: e.target.value })} /> updateFlowField(sectionIndex, fieldIndex, { label: e.target.value })} /> 启用 updateFlowField(sectionIndex, fieldIndex, { enabled: checked })} /> 编辑 updateFlowField(sectionIndex, fieldIndex, { editable: checked })} /> updateFlowField(sectionIndex, fieldIndex, { maxLength: Number(value || 800) })} /> updateFlowField(sectionIndex, fieldIndex, { value: e.target.value })} />