239 lines
9.5 KiB
TypeScript
239 lines
9.5 KiB
TypeScript
import React from 'react';
|
|
import { Alert, Input, Radio, Space, Tag, Typography } from 'antd';
|
|
import type {
|
|
JsonValue,
|
|
VideoPromptSchemaFieldConfig,
|
|
VideoPromptSchemaSectionConfig,
|
|
} from '../utils/videoPromptSchema';
|
|
import {
|
|
clonePlain,
|
|
hasSchemaConfigSnapshot,
|
|
normalizeSchemaSections,
|
|
setArrayObjectField,
|
|
setObjectField,
|
|
shouldUseSchemaTextArea,
|
|
stringifySchemaValue,
|
|
} from '../utils/videoPromptSchema';
|
|
|
|
const { Text } = Typography;
|
|
const { TextArea } = Input;
|
|
|
|
type VideoPromptSchemaEditorProps = {
|
|
value: Record<string, JsonValue>;
|
|
schemaConfigSnapshot?: Record<string, any> | null;
|
|
onChange: (nextValue: Record<string, JsonValue>) => void;
|
|
};
|
|
|
|
function isRecord(value: unknown): value is Record<string, JsonValue> {
|
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function ReadonlyBlock({ value }: { value: JsonValue }) {
|
|
const text = stringifySchemaValue(value);
|
|
return shouldUseSchemaTextArea(value) ? (
|
|
<TextArea value={text} rows={Math.min(6, Math.max(2, Math.ceil(text.length / 42)))} disabled style={{ borderRadius: 8, color: '#64748b' }} />
|
|
) : (
|
|
<Input value={text} disabled style={{ borderRadius: 8, color: '#64748b' }} />
|
|
);
|
|
}
|
|
|
|
function EditableInput({ value, maxLength, onChange }: { value: JsonValue; maxLength?: number; onChange: (nextValue: JsonValue) => void }) {
|
|
const text = stringifySchemaValue(value);
|
|
const commonProps = {
|
|
value: text,
|
|
maxLength,
|
|
showCount: !!maxLength,
|
|
onChange: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => onChange(event.target.value),
|
|
style: { borderRadius: 8 },
|
|
};
|
|
|
|
if (shouldUseSchemaTextArea(value)) {
|
|
return <TextArea {...commonProps} rows={Math.min(8, Math.max(3, Math.ceil(text.length / 42)))} />;
|
|
}
|
|
|
|
return <Input {...commonProps} />;
|
|
}
|
|
|
|
function BooleanRadio({ value, onChange }: { value: JsonValue; onChange: (nextValue: JsonValue) => void }) {
|
|
const boolValue = value === true || value === 'true';
|
|
return (
|
|
<Radio.Group
|
|
value={boolValue}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
style={{ display: 'flex', gap: 24 }}
|
|
>
|
|
<Radio value={true} style={{ fontSize: 14, color: '#475569' }}>是</Radio>
|
|
<Radio value={false} style={{ fontSize: 14, color: '#475569' }}>否</Radio>
|
|
</Radio.Group>
|
|
);
|
|
}
|
|
|
|
// function renderFieldTag(field: VideoPromptSchemaFieldConfig) {
|
|
// return field.editable ? <Tag color="processing">可编辑</Tag> : <Tag color="default">只读</Tag>;
|
|
// }
|
|
|
|
const VideoPromptSchemaEditor: React.FC<VideoPromptSchemaEditorProps> = ({ value, schemaConfigSnapshot, onChange }) => {
|
|
const safeValue = isRecord(value) ? value : {};
|
|
const sections = normalizeSchemaSections(schemaConfigSnapshot);
|
|
|
|
if (!hasSchemaConfigSnapshot(schemaConfigSnapshot)) {
|
|
return (
|
|
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
|
<Alert
|
|
type="warning"
|
|
showIcon
|
|
message="视频提词配置缺失,暂不能编辑"
|
|
description="请刷新详情后重试;如果仍然缺失,请联系管理员排查第4步 schema_config_snapshot 返回。"
|
|
/>
|
|
{Object.keys(safeValue).length ? (
|
|
<pre style={{ margin: 0, padding: 12, borderRadius: 10, background: '#f8fafc', maxHeight: 420, overflow: 'auto', color: '#475569' }}>
|
|
{JSON.stringify(safeValue, null, 2)}
|
|
</pre>
|
|
) : null}
|
|
</Space>
|
|
);
|
|
}
|
|
|
|
if (!sections.length) {
|
|
return <Alert type="warning" showIcon message="视频提词配置为空" description="当前 Schema 配置没有可展示字段。" />;
|
|
}
|
|
|
|
const renderObjectSection = (section: VideoPromptSchemaSectionConfig) => {
|
|
const sectionValue = isRecord(safeValue[section.key]) ? safeValue[section.key] as Record<string, JsonValue> : {};
|
|
const fields = section.children.filter((field) => field.enabled);
|
|
if (!fields.length) return null;
|
|
|
|
return (
|
|
<div key={section.key} style={{ marginBottom: 18 }}>
|
|
<Space style={{ marginBottom: 8 }}>
|
|
<Text strong style={{ color: '#334155', fontSize: 13 }}>{section.label}</Text>
|
|
{/* {renderFieldTag(section)} */}
|
|
</Space>
|
|
<div style={{ borderLeft: '3px solid #6366f1', paddingLeft: 12, marginLeft: 4 }}>
|
|
{fields.map((field) => (
|
|
<div key={field.key} style={{ marginBottom: 12 }}>
|
|
<Space style={{ marginBottom: 4 }}>
|
|
<Text style={{ color: '#64748b', fontSize: 12 }}>{field.label}</Text>
|
|
{/* {renderFieldTag(field)} */}
|
|
</Space>
|
|
{field.editable ? (
|
|
field.type === 'boolean' ? (
|
|
<BooleanRadio
|
|
value={sectionValue[field.key]}
|
|
onChange={(nextValue) => onChange(setObjectField(safeValue, section.key, field.key, nextValue))}
|
|
/>
|
|
) : (
|
|
<EditableInput
|
|
value={sectionValue[field.key]}
|
|
maxLength={field.maxLength}
|
|
onChange={(nextText) => onChange(setObjectField(safeValue, section.key, field.key, nextText))}
|
|
/>
|
|
)
|
|
) : (
|
|
<ReadonlyBlock value={sectionValue[field.key]} />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderFlowSection = (section: VideoPromptSchemaSectionConfig) => {
|
|
const rows = Array.isArray(safeValue[section.key]) ? safeValue[section.key] as Record<string, JsonValue>[] : [];
|
|
const fields = section.itemFields.filter((field) => field.enabled);
|
|
const displayFields = [{ key: '时间段', label: '时间段', enabled: true, editable: false } as VideoPromptSchemaFieldConfig, ...fields];
|
|
|
|
return (
|
|
<div key={section.key} style={{ marginBottom: 18 }}>
|
|
<Space style={{ marginBottom: 8 }}>
|
|
<Text strong style={{ color: '#334155', fontSize: 13 }}>{section.label}</Text>
|
|
<Tag color="default">锁定条目和时间段</Tag>
|
|
</Space>
|
|
<div style={{ border: '1px solid #e5e7eb', borderRadius: 10, padding: 12, background: '#fff' }}>
|
|
{rows.length === 0 ? (
|
|
<Text style={{ color: '#94a3b8', fontSize: 12 }}>暂无内容</Text>
|
|
) : (
|
|
rows.map((item, index) => {
|
|
const row = isRecord(item) ? item : {};
|
|
return (
|
|
<div key={`${section.key}-${index}`} style={{ marginBottom: index === rows.length - 1 ? 0 : 14, paddingBottom: index === rows.length - 1 ? 0 : 14, borderBottom: index === rows.length - 1 ? 'none' : '1px dashed #e2e8f0' }}>
|
|
<Text strong style={{ display: 'block', marginBottom: 8, color: '#475569', fontSize: 12 }}>第 {index + 1} 段</Text>
|
|
{displayFields.map((field) => (
|
|
<div key={field.key} style={{ marginBottom: 10 }}>
|
|
<Space style={{ marginBottom: 4 }}>
|
|
<Text style={{ color: '#64748b', fontSize: 12 }}>{field.label}</Text>
|
|
{/* {renderFieldTag(field)} */}
|
|
</Space>
|
|
{field.editable ? (
|
|
field.type === 'boolean' ? (
|
|
<BooleanRadio
|
|
value={row[field.key]}
|
|
onChange={(nextValue) => onChange(setArrayObjectField(safeValue, section.key, index, field.key, nextValue))}
|
|
/>
|
|
) : (
|
|
<EditableInput
|
|
value={row[field.key]}
|
|
maxLength={field.maxLength}
|
|
onChange={(nextText) => onChange(setArrayObjectField(safeValue, section.key, index, field.key, nextText))}
|
|
/>
|
|
)
|
|
) : (
|
|
<ReadonlyBlock value={row[field.key]} />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderPrimitiveSection = (section: VideoPromptSchemaSectionConfig) => {
|
|
if (!(section.key in safeValue)) return null;
|
|
return (
|
|
<div key={section.key} style={{ marginBottom: 18 }}>
|
|
<Space style={{ marginBottom: 8 }}>
|
|
<Text strong style={{ color: '#334155', fontSize: 13 }}>{section.label}</Text>
|
|
{/* {renderFieldTag(section)} */}
|
|
</Space>
|
|
{section.editable ? (
|
|
<EditableInput
|
|
value={safeValue[section.key]}
|
|
maxLength={section.maxLength}
|
|
onChange={(nextText) => {
|
|
const next = clonePlain(safeValue);
|
|
next[section.key] = nextText;
|
|
onChange(next);
|
|
}}
|
|
/>
|
|
) : (
|
|
<ReadonlyBlock value={safeValue[section.key]} />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
{/* <Alert
|
|
type="info"
|
|
showIcon
|
|
style={{ marginBottom: 14 }}
|
|
message="仅可修改后台 Schema 配置允许编辑的字段"
|
|
description="视频规格、时间段、流程条目数量、输出规格、质量控制、合规控制等锁定内容由服务端最终校验。"
|
|
/> */}
|
|
{sections.map((section) => {
|
|
if (!(section.key in safeValue)) return null;
|
|
if (section.type === 'object') return renderObjectSection(section);
|
|
if (section.type === 'flow') return renderFlowSection(section);
|
|
return renderPrimitiveSection(section);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default VideoPromptSchemaEditor; |