dev app commit

This commit is contained in:
2026-06-17 17:55:02 +08:00
parent 725649f75c
commit 2142f78119
5 changed files with 1150 additions and 290 deletions
@@ -0,0 +1,274 @@
import React from 'react';
import { Button, Input, Space, Tag, Typography } from 'antd';
const { Text } = Typography;
const { TextArea } = Input;
type JsonValue = any;
type VideoPromptSchemaEditorProps = {
value: Record<string, JsonValue>;
onChange: (nextValue: Record<string, JsonValue>) => void;
};
const LOCKED_TOP_LEVEL_KEYS = new Set([
'schema_version',
'schema_usage',
'动态时间规划',
'输出规格限制',
'合规控制',
'质量控制',
]);
const LOCKED_FRAME_KEYS = new Set([
'视频时长',
'视频比例',
'清晰度',
'帧率',
'推荐分辨率',
]);
const EDITABLE_FRAME_KEYS = new Set([
'主体描述',
'主体数量',
'主体位置',
'主体占比',
'场景描述',
'构图方式',
'画面风格',
'光影色彩',
]);
const EDITABLE_FINAL_PROMPT_KEYS = new Set([
'主提示词',
'动作提示词',
'镜头提示词',
'字幕提示词',
'音频提示词',
'风格提示词',
'负面提示词',
]);
function clonePlain<T>(value: T): T {
return value === undefined ? value : JSON.parse(JSON.stringify(value));
}
function pathIncludes(path: Array<string | number>, key: string): boolean {
return path.some((item) => String(item) === key);
}
function getPathValue(root: JsonValue, path: Array<string | number>): JsonValue {
let current = root;
for (const key of path) {
if (current === undefined || current === null) return undefined;
current = current[key as keyof typeof current];
}
return current;
}
function setPathValue(root: JsonValue, path: Array<string | number>, value: JsonValue): JsonValue {
const next = clonePlain(root);
let current = next;
for (let index = 0; index < path.length - 1; index += 1) {
current = current[path[index] as keyof typeof current];
}
current[path[path.length - 1] as keyof typeof current] = value;
return next;
}
function removeArrayItem(root: JsonValue, path: Array<string | number>, index: number): JsonValue {
const arrayValue = getPathValue(root, path);
if (!Array.isArray(arrayValue)) return root;
return setPathValue(root, path, arrayValue.filter((_, itemIndex) => itemIndex !== index));
}
function addArrayItem(root: JsonValue, path: Array<string | number>, sampleValue: JsonValue): JsonValue {
const arrayValue = getPathValue(root, path);
if (!Array.isArray(arrayValue)) return root;
const nextItem = typeof sampleValue === 'object' && sampleValue !== null ? clonePlain(sampleValue) : '';
return setPathValue(root, path, [...arrayValue, nextItem]);
}
function stringifyReadonly(value: JsonValue): string {
if (value === null || value === undefined) return '';
if (typeof value === 'object') return JSON.stringify(value, null, 2);
return String(value);
}
function shouldUseTextArea(value: JsonValue): boolean {
const text = stringifyReadonly(value);
return text.length > 40 || text.includes('\n') || text.includes('') || text.includes('。');
}
function isLockedPath(path: Array<string | number>): boolean {
const rootKey = String(path[0] ?? '');
const currentKey = String(path[path.length - 1] ?? '');
if (LOCKED_TOP_LEVEL_KEYS.has(rootKey)) return true;
if (rootKey === '画面属性') {
if (LOCKED_FRAME_KEYS.has(currentKey)) return true;
if (!EDITABLE_FRAME_KEYS.has(currentKey)) return false;
}
if (rootKey === '最终提示词' && path.length === 2) {
return !EDITABLE_FINAL_PROMPT_KEYS.has(currentKey);
}
if ((rootKey === '动作流程' || rootKey === '镜头流程') && currentKey === '时间段') {
return true;
}
return false;
}
function canAddOrRemoveArray(path: Array<string | number>): boolean {
const rootKey = String(path[0] ?? '');
if (LOCKED_TOP_LEVEL_KEYS.has(rootKey)) return false;
if (rootKey === '动作流程' || rootKey === '镜头流程') return false;
return true;
}
function fieldTitle(key: string | number): string {
return typeof key === 'number' ? `${key + 1}` : key;
}
function ReadonlyBlock({ value }: { value: JsonValue }) {
const text = stringifyReadonly(value);
return shouldUseTextArea(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, onChange }: { value: JsonValue; onChange: (nextValue: JsonValue) => void }) {
const text = stringifyReadonly(value);
if (shouldUseTextArea(value)) {
return (
<TextArea
value={text}
onChange={(event) => onChange(event.target.value)}
rows={Math.min(8, Math.max(3, Math.ceil(text.length / 42)))}
style={{ borderRadius: 8 }}
/>
);
}
return <Input value={text} onChange={(event) => onChange(event.target.value)} style={{ borderRadius: 8 }} />;
}
const VideoPromptSchemaEditor: React.FC<VideoPromptSchemaEditorProps> = ({ value, onChange }) => {
const safeValue = value && typeof value === 'object' ? value : {};
const updatePath = (path: Array<string | number>, nextValue: JsonValue) => {
onChange(setPathValue(safeValue, path, nextValue));
};
const renderNode = (key: string | number, nodeValue: JsonValue, path: Array<string | number>, depth = 0): React.ReactNode => {
const locked = isLockedPath(path);
const rootKey = String(path[0] ?? '');
if (Array.isArray(nodeValue)) {
const editableArray = !locked && canAddOrRemoveArray(path);
const sample = nodeValue.find((item) => item !== undefined) ?? '';
return (
<div key={path.join('.')} style={{ marginBottom: 18 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
<Space>
<Text strong style={{ color: '#334155', fontSize: 13 }}>{fieldTitle(key)}</Text>
{!editableArray && <Tag color="default"></Tag>}
</Space>
{editableArray && (
<Button size="small" type="link" onClick={() => onChange(addArrayItem(safeValue, path, sample))}>
+
</Button>
)}
</div>
<div style={{ border: '1px solid #e5e7eb', borderRadius: 10, padding: 12, background: locked ? '#f8fafc' : '#fff' }}>
{nodeValue.length === 0 ? (
<Text style={{ color: '#94a3b8', fontSize: 12 }}></Text>
) : (
nodeValue.map((item, index) => (
<div key={`${path.join('.')}.${index}`} style={{ marginBottom: index === nodeValue.length - 1 ? 0 : 12 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }}>
<Text style={{ color: '#94a3b8', fontSize: 12, marginTop: 7, minWidth: 28 }}>{index + 1}.</Text>
<div style={{ flex: 1 }}>
{typeof item === 'object' && item !== null ? (
renderObjectFields(item, [...path, index], depth + 1)
) : locked ? (
<ReadonlyBlock value={item} />
) : (
<EditableInput value={item} onChange={(nextText) => updatePath([...path, index], nextText)} />
)}
</div>
{editableArray && (
<Button size="small" type="text" danger onClick={() => onChange(removeArrayItem(safeValue, path, index))}>
</Button>
)}
</div>
</div>
))
)}
</div>
{(rootKey === '动作流程' || rootKey === '镜头流程') && (
<Text style={{ display: 'block', marginTop: 6, color: '#94a3b8', fontSize: 12 }}>
</Text>
)}
</div>
);
}
if (typeof nodeValue === 'object' && nodeValue !== null) {
const lockedSection = locked || LOCKED_TOP_LEVEL_KEYS.has(String(key));
return (
<div key={path.join('.')} style={{ marginBottom: 18 }}>
<Space style={{ marginBottom: 8 }}>
<Text strong style={{ color: '#334155', fontSize: 13 }}>{fieldTitle(key)}</Text>
{lockedSection && <Tag color="default"></Tag>}
</Space>
<div
style={{
borderLeft: depth === 0 ? '3px solid #6366f1' : '2px solid #e2e8f0',
paddingLeft: 12,
marginLeft: 4,
background: lockedSection ? '#f8fafc' : 'transparent',
}}
>
{renderObjectFields(nodeValue, path, depth + 1)}
</div>
</div>
);
}
return (
<div key={path.join('.')} style={{ marginBottom: 12 }}>
<Space style={{ marginBottom: 4 }}>
<Text style={{ color: '#64748b', fontSize: 12 }}>{fieldTitle(key)}</Text>
{locked && <Tag color="default"></Tag>}
</Space>
{locked ? (
<ReadonlyBlock value={nodeValue} />
) : (
<EditableInput value={nodeValue} onChange={(nextText) => updatePath(path, nextText)} />
)}
</div>
);
};
const renderObjectFields = (objectValue: Record<string, JsonValue>, parentPath: Array<string | number>, depth = 0): React.ReactNode => {
return Object.entries(objectValue).map(([childKey, childValue]) => renderNode(childKey, childValue, [...parentPath, childKey], depth));
};
return (
<div>
<div style={{ marginBottom: 14, padding: 12, borderRadius: 10, background: '#f8fafc', color: '#64748b', fontSize: 13, lineHeight: 1.7 }}>
schema /
</div>
{Object.entries(safeValue).map(([key, childValue]) => renderNode(key, childValue, [key]))}
</div>
);
};
export default VideoPromptSchemaEditor;