@@ -0,0 +1,707 @@
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' } ,
] ;
function clone < T > ( value : T ) : T {
return JSON . parse ( JSON . stringify ( value ? ? null ) ) ;
}
function pick < T = any > ( 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 < any [ ] > ( node , 'contentAliases' , 'content_aliases' , [ ] ) ;
normalized . contentAliases = Array . isArray ( aliases ) ? aliases . map ( item = > String ( item ) ) . filter ( Boolean ) : [ ] ;
const itemFields = pick < any [ ] > ( 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 ) : any {
const base : any = {
key : node.key ,
label : node.label ,
type : node . type ,
enabled : ! ! node . enabled ,
editable : ! ! node . editable ,
max_length : node.maxLength ,
} ;
if ( node . type === 'object' ) {
base . children = ( node . children || [ ] ) . map ( toApiNode ) ;
} 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 ( toApiNode ) ,
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 < VideoPromptSchemaConfigData | null > ( null ) ;
const [ defaultConfig , setDefaultConfig ] = useState < VideoPromptSchemaConfigData | null > ( null ) ;
const [ previewOpen , setPreviewOpen ] = useState ( false ) ;
const [ previewResult , setPreviewResult ] = useState < any > ( null ) ;
const [ previewForm ] = Form . useForm ( ) ;
const importInputRef = useRef < HTMLInputElement | null > ( 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 < HTMLInputElement > ) = > {
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 < VideoPromptSchemaNode > ) = > {
updateConfig ( draft = > {
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 < VideoPromptSchemaNode > ) = > {
updateConfig ( draft = > {
const children = draft . sections [ sectionIndex ] . children || [ ] ;
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 < VideoPromptFlowItemField > ) = > {
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 < VideoPromptTimePlanRule > ) = > {
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 . key } - ${ sectionIndex } ` ,
label : (
< Space >
< Text strong > { section . label || section . key || '未命名分组' } < / Text >
{ section . key && section . key !== section . label && < Tag color = "blue" > key : { section . key } < / Tag > }
< Tag > { section . type } < / Tag >
{ ! section . enabled && < Tag color = "default" > 已 禁 用 < / Tag > }
< / Space >
) ,
children : (
< Space direction = "vertical" style = { { width : '100%' } } size = "middle" >
< Row gutter = { 12 } align = "middle" >
< Col span = { 4 } > < Input addonBefore = "key" value = { section . key } maxLength = { 64 } placeholder = "传给 AI 的字段名" onChange = { e = > updateSection ( sectionIndex , { key : e.target.value } ) } / > < / Col >
< Col span = { 4 } > < Input addonBefore = "label" value = { section . label } maxLength = { 64 } placeholder = "后台显示名称" onChange = { e = > updateSection ( sectionIndex , { label : e.target.value } ) } / > < / Col >
< Col span = { 4 } >
< Select value = { section . type as any } options = { NODE_TYPES } style = { { width : '100%' } } onChange = { value = > updateSection ( sectionIndex , { type : value } ) } / >
< / Col >
< Col span = { 3 } > < Space > 启 用 < Switch checked = { section . enabled } onChange = { checked = > updateSection ( sectionIndex , { enabled : checked } ) } / > < / Space > < / Col >
< Col span = { 3 } > < Space > 可 编 辑 < Switch checked = { section . editable } onChange = { checked = > updateSection ( sectionIndex , { editable : checked } ) } / > < / Space > < / Col >
< Col span = { 4 } > < InputNumber min = { 1 } max = { 20000 } value = { section . maxLength } addonBefore = "长度" style = { { width : '100%' } } onChange = { value = > updateSection ( sectionIndex , { maxLength : Number ( value || 1000 ) } ) } / > < / Col >
< Col span = { 2 } style = { { textAlign : 'right' } } >
< Popconfirm title = "确认删除该分组?" onConfirm = { ( ) = > deleteSection ( sectionIndex ) } >
< Button danger icon = { < DeleteOutlined / > } / >
< / Popconfirm >
< / Col >
< / Row >
{ section . type === 'object' && (
< Card size = "small" title = "对象字段" extra = { < Button size = "small" icon = { < PlusOutlined / > } onClick = { ( ) = > addChild ( sectionIndex ) } > 新 增 字 段 < / Button > } >
< Space direction = "vertical" style = { { width : '100%' } } >
{ ( section . children || [ ] ) . map ( ( child , childIndex ) = > (
< Row gutter = { 8 } align = "middle" key = { ` ${ child . key } - ${ childIndex } ` } >
< Col span = { 4 } > < Input addonBefore = "key" value = { child . key } maxLength = { 64 } placeholder = "传给 AI 的字段名" onChange = { e = > updateChild ( sectionIndex , childIndex , { key : e.target.value } ) } / > < / Col >
< Col span = { 4 } > < Input addonBefore = "label" value = { child . label } maxLength = { 64 } placeholder = "后台显示名称" onChange = { e = > updateChild ( sectionIndex , childIndex , { label : e.target.value } ) } / > < / Col >
< Col span = { 3 } > < Select value = { child . type as any } options = { NODE_TYPES . filter ( item = > item . value !== 'object' && item . value !== 'flow' ) } style = { { width : '100%' } } onChange = { value = > updateChild ( sectionIndex , childIndex , { type : value } ) } / > < / Col >
< Col span = { 2 } > < Space > 启 用 < Switch checked = { child . enabled } onChange = { checked = > updateChild ( sectionIndex , childIndex , { enabled : checked } ) } / > < / Space > < / Col >
< Col span = { 2 } > < Space > 编 辑 < Switch checked = { child . editable } onChange = { checked = > updateChild ( sectionIndex , childIndex , { editable : checked } ) } / > < / Space > < / Col >
< Col span = { 3 } > < InputNumber min = { 1 } max = { 20000 } value = { child . maxLength } addonBefore = "长度" style = { { width : '100%' } } onChange = { value = > updateChild ( sectionIndex , childIndex , { maxLength : Number ( value || 1000 ) } ) } / > < / Col >
< Col span = { 5 } >
< Input value = { Array . isArray ( child . value ) ? child . value . join ( '/' ) : String ( child . value ? ? '' ) } maxLength = { child . maxLength || 1000 } placeholder = "默认值" onChange = { e = > updateChild ( sectionIndex , childIndex , { value : child.type === 'array' ? e . target . value . split ( '/' ) . filter ( Boolean ) : e . target . value } ) } / >
< / Col >
< Col span = { 1 } style = { { textAlign : 'right' } } >
< Button danger size = "small" icon = { < DeleteOutlined / > } onClick = { ( ) = > deleteChild ( sectionIndex , childIndex ) } / >
< / Col >
< / Row >
) ) }
< / Space >
< / Card >
) }
{ section . type === 'flow' && (
< Card size = "small" title = "流程数组对象字段" extra = { < Button size = "small" icon = { < PlusOutlined / > } onClick = { ( ) = > addFlowField ( sectionIndex ) } > 新 增 流 程 字 段 < / Button > } >
< Row gutter = { 12 } style = { { marginBottom : 12 } } >
< Col span = { 6 } > < Input addonBefore = "内容字段" value = { section . contentKey } maxLength = { 64 } onChange = { e = > updateSection ( sectionIndex , { contentKey : e.target.value } ) } / > < / Col >
< Col span = { 18 } > < Input addonBefore = "别名 / 分隔" value = { ( section . contentAliases || [ ] ) . join ( '/' ) } onChange = { e = > updateSection ( sectionIndex , { contentAliases : e.target.value.split ( '/' ) . map ( item = > item . trim ( ) ) . filter ( Boolean ) } ) } / > < / Col >
< / Row >
< Space direction = "vertical" style = { { width : '100%' } } >
{ ( section . itemFields || [ ] ) . map ( ( field , fieldIndex ) = > (
< Row gutter = { 8 } align = "middle" key = { ` ${ field . key } - ${ fieldIndex } ` } >
< Col span = { 5 } > < Input addonBefore = "key" value = { field . key } maxLength = { 64 } placeholder = "流程对象字段名" onChange = { e = > updateFlowField ( sectionIndex , fieldIndex , { key : e.target.value } ) } / > < / Col >
< Col span = { 5 } > < Input addonBefore = "label" value = { field . label } maxLength = { 64 } placeholder = "后台显示名称" onChange = { e = > updateFlowField ( sectionIndex , fieldIndex , { label : e.target.value } ) } / > < / Col >
< Col span = { 3 } > < Space > 启 用 < Switch checked = { field . enabled } onChange = { checked = > updateFlowField ( sectionIndex , fieldIndex , { enabled : checked } ) } / > < / Space > < / Col >
< Col span = { 3 } > < Space > 编 辑 < Switch checked = { field . editable } onChange = { checked = > updateFlowField ( sectionIndex , fieldIndex , { editable : checked } ) } / > < / Space > < / Col >
< Col span = { 4 } > < InputNumber min = { 1 } max = { 20000 } value = { field . maxLength } addonBefore = "长度" style = { { width : '100%' } } onChange = { value = > updateFlowField ( sectionIndex , fieldIndex , { maxLength : Number ( value || 800 ) } ) } / > < / Col >
< Col span = { 3 } > < Input value = { String ( field . value ? ? '' ) } maxLength = { field . maxLength || 800 } placeholder = "默认值" onChange = { e = > updateFlowField ( sectionIndex , fieldIndex , { value : e.target.value } ) } / > < / Col >
< Col span = { 1 } style = { { textAlign : 'right' } } > < Button danger size = "small" icon = { < DeleteOutlined / > } onClick = { ( ) = > deleteFlowField ( sectionIndex , fieldIndex ) } / > < / Col >
< / Row >
) ) }
< / Space >
< / Card >
) }
{ section . type !== 'object' && section . type !== 'flow' && (
< Card size = "small" title = "默认值" >
< TextArea rows = { 3 } value = { Array . isArray ( section . value ) ? section . value . join ( '\n' ) : String ( section . value ? ? '' ) } maxLength = { section . maxLength || 1000 } showCount onChange = { e = > updateSection ( sectionIndex , { value : section.type === 'array' ? e . target . value . split ( '\n' ) . filter ( Boolean ) : e . target . value } ) } / >
< / Card >
) }
< / Space >
) ,
} ) ) , [ sections ] ) ;
if ( loading || ! config ) {
return < Spin / > ;
}
return (
< div style = { { padding : 24 } } >
< Card >
< Space direction = "vertical" style = { { width : '100%' } } size = "large" >
< Row justify = "space-between" align = "middle" >
< Col >
< Title level = { 3 } style = { { margin : 0 } } > 视 频 提 词 Schema 配 置 < / Title >
< Paragraph type = "secondary" style = { { marginBottom : 0 } } >
配 置 会 被 爆 款 开 头 复 刻 和 拆 镜 复 刻 第 4 步 视 频 AI 提 词 共 同 复 用 。 传 给 AI 时 只 保 留 纯 键 值 schema , 不 携 带 显 示 名 、 启 用 、 可 编 辑 等 管 理 字 段 。
< / Paragraph >
< / Col >
< Col >
< Space >
< Text > 启 用 自 定 义 配 置 < / Text >
< Switch checked = { enabled } onChange = { setEnabled } / >
{ usingDefault && < Tag color = "orange" > 当 前 使 用 默 认 配 置 兜 底 < / Tag > }
{ defaultConfig && < Tag > 默 认 分 组 { defaultConfig . sections . length } < / Tag > }
< / Space >
< / Col >
< / Row >
< Alert
type = "info"
showIcon
message = "长度限制策略"
description = "只对管理后台字段和用户可编辑字段做长度校验,不会自动裁剪 AI 生成结果或第 5 步最终视频 prompt。若供应商因为 prompt 过长拒绝,会按真实失败进入现有日志/退款/重试流程。"
/ >
< Alert
type = "warning"
showIcon
message = "预览说明"
description = "预览会跟随右上角「启用自定义配置」开关。关闭时会按默认 CLIENT_SCHEMA_V1 兜底预览;打开后才会按当前页面编辑的 sections 和秒数切片规则预览。"
/ >
< Space wrap >
< Button type = "primary" icon = { < SaveOutlined / > } loading = { saving } onClick = { save } > 保 存 配 置 < / Button >
< Button icon = { < EyeOutlined / > } onClick = { openPreview } > 预 览 运 行 时 Schema < / Button >
< Button icon = { < DownloadOutlined / > } onClick = { doExport } > 导 出 JSON < / Button >
< Button icon = { < UploadOutlined / > } onClick = { ( ) = > importInputRef . current ? . click ( ) } > 导 入 JSON < / Button >
< Popconfirm title = "确认恢复默认配置?" description = "会用 CLIENT_SCHEMA_V1 生成的默认配置覆盖当前配置。" onConfirm = { resetDefault } >
< Button icon = { < ReloadOutlined / > } > 恢 复 默 认 < / Button >
< / Popconfirm >
< input ref = { importInputRef } type = "file" accept = "application/json,.json" style = { { display : 'none' } } onChange = { onImportFile } / >
< / Space >
< Tabs
items = { [
{
key : 'fields' ,
label : 'Schema 字段配置' ,
children : (
< Space direction = "vertical" style = { { width : '100%' } } size = "middle" >
< Alert
type = "warning"
showIcon
message = "字段说明"
description = "key 是最终传给 AI 和保存到 prompt_schema 的真实字段名,例如「素材理解」「动作流程」;label 只用于管理后台显示,帮助运营/管理员理解这个字段,不会传给 AI;启用关闭后该字段不会进入运行时 schema;可编辑关闭后用户在第 4 步修改时不能改这个字段。"
/ >
< Button icon = { < PlusOutlined / > } onClick = { addSection } > 新 增 一 级 分 组 < / Button >
< Collapse items = { sectionItems } / >
< / Space >
) ,
} ,
{
key : 'time' ,
label : '秒数切片规则' ,
children : (
< Space direction = "vertical" style = { { width : '100%' } } size = "middle" >
< Alert
type = "info"
showIcon
message = "切片规则说明"
description = "这里决定指定视频时长会被拆成几段,以及每段的阶段名称和说明。运行时会同步生成「动态时间规划」「动作流程」「镜头流程」三个数组,三者时间段保持一致。"
/ >
< Button icon = { < PlusOutlined / > } onClick = { addTimeRule } > 新 增 秒 数 规 则 < / Button >
{ timePlanRules . map ( ( rule , ruleIndex ) = > (
< Card
key = { ruleIndex }
size = "small"
title = { ` ${ rule . minDuration } - ${ rule . maxDuration } 秒: ${ rule . segments . length } 段 ` }
extra = { < Button danger size = "small" icon = { < DeleteOutlined / > } onClick = { ( ) = > deleteTimeRule ( ruleIndex ) } > 删 除 规 则 < / Button > }
>
< Row gutter = { 12 } style = { { marginBottom : 12 } } >
< Col span = { 4 } > < InputNumber min = { 1 } value = { rule . minDuration } addonBefore = "最小秒" style = { { width : '100%' } } onChange = { value = > updateTimeRule ( ruleIndex , { minDuration : Number ( value || 1 ) } ) } / > < / Col >
< Col span = { 4 } > < InputNumber min = { 1 } value = { rule . maxDuration } addonBefore = "最大秒" style = { { width : '100%' } } onChange = { value = > updateTimeRule ( ruleIndex , { maxDuration : Number ( value || 1 ) } ) } / > < / Col >
< Col span = { 16 } > < Button size = "small" icon = { < PlusOutlined / > } onClick = { ( ) = > addSegment ( ruleIndex ) } > 新 增 片 段 < / Button > < / Col >
< / Row >
< Space direction = "vertical" style = { { width : '100%' } } >
{ rule . segments . map ( ( segment , segmentIndex ) = > (
< Row gutter = { 8 } align = "middle" key = { segmentIndex } >
< Col span = { 3 } > < InputNumber min = { 0.01 } step = { 0.01 } value = { rule . ratios [ segmentIndex ] } addonBefore = "比例" style = { { width : '100%' } } onChange = { value = > updateSegment ( ruleIndex , segmentIndex , { ratio : Number ( value || 0.01 ) } ) } / > < / Col >
< Col span = { 5 } > < Input value = { segment . stage } maxLength = { 64 } placeholder = "阶段" onChange = { e = > updateSegment ( ruleIndex , segmentIndex , { stage : e.target.value } ) } / > < / Col >
< Col span = { 15 } > < Input value = { segment . description } maxLength = { 300 } placeholder = "说明" onChange = { e = > updateSegment ( ruleIndex , segmentIndex , { description : e.target.value } ) } / > < / Col >
< Col span = { 1 } > < Button danger size = "small" icon = { < DeleteOutlined / > } onClick = { ( ) = > deleteSegment ( ruleIndex , segmentIndex ) } / > < / Col >
< / Row >
) ) }
< / Space >
< / Card >
) ) }
< / Space >
) ,
} ,
{
key : 'json' ,
label : 'JSON 高级查看' ,
children : < TextArea rows = { 24 } value = { JSON . stringify ( toApiConfig ( config , enabled ) , null , 2 ) } readOnly / > ,
} ,
] }
/ >
< / Space >
< / Card >
< Modal title = "预览运行时 Schema" open = { previewOpen } onCancel = { ( ) = > setPreviewOpen ( false ) } width = { 980 } footer = { < Space > < Button onClick = { ( ) = > setPreviewOpen ( false ) } > 关 闭 < / Button > < Button type = "primary" onClick = { doPreview } > 生 成 预 览 < / Button > < / Space > } >
< Form form = { previewForm } layout = "inline" style = { { marginBottom : 16 } } >
< Form.Item name = "duration" label = "时长" rules = { [ { required : true } ] } > < InputNumber min = { 1 } max = { 3600 } / > < / Form.Item >
< Form.Item name = "aspectRatio" label = "比例" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "resolution" label = "清晰度" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "frameRate" label = "帧率" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< / Form >
< Divider / >
{ previewResult ? (
< Tabs
items = { [
{
key : 'runtime' ,
label : '运行时 Schema' ,
children : < TextArea rows = { 24 } value = { JSON . stringify ( runtimePreviewSchema ( previewResult ) , null , 2 ) } readOnly / > ,
} ,
{
key : 'timePlan' ,
label : ` 动态时间规划( ${ runtimePreviewTimePlan ( previewResult ) . length } 段) ` ,
children : < TextArea rows = { 24 } value = { JSON . stringify ( runtimePreviewTimePlan ( previewResult ) , null , 2 ) } readOnly / > ,
} ,
{
key : 'snapshot' ,
label : '配置快照' ,
children : < TextArea rows = { 24 } value = { JSON . stringify ( runtimePreviewSnapshot ( previewResult ) , null , 2 ) } readOnly / > ,
} ,
] }
/ >
) : (
< Alert type = "info" showIcon message = "点击生成预览后,可以看到传给 AI 的纯 schema、动态时间规划和本次配置快照。" / >
) }
< / Modal >
< / div >
) ;
} ;
export default AdminVideoPromptSchemaConfig ;