diff --git a/apps/web/src/api/editingPlanner.ts b/apps/web/src/api/editingPlanner.ts index b6763c0de..063acbc9f 100644 --- a/apps/web/src/api/editingPlanner.ts +++ b/apps/web/src/api/editingPlanner.ts @@ -6,8 +6,24 @@ /* ──────────── 类型定义 ──────────── */ -/** 模板模式 */ -export type TemplateMode = '画中画' | '人物口播' | '一镜到底' | '口播+混剪'; +/** 模板模式(后端枚举值) */ +export type TemplateMode = 'pip' | 'voice_over' | 'one_take' | 'voice_pip'; + +/** 模式显示名称映射 */ +export const MODE_LABELS: Record = { + pip: '画中画', + voice_over: '人物口播', + one_take: '一镜到底', + voice_pip: '口播+混剪', +}; + +/** 模式颜色映射 */ +export const MODE_COLORS: Record = { + pip: 'blue', + voice_over: 'green', + one_take: 'orange', + voice_pip: 'purple', +}; /** 标题配置 */ export interface TitleConfig { @@ -75,12 +91,13 @@ export interface SaveTemplatePayload { title_config: TitleConfig; subtitle_config: SubtitleConfig; bgm_config: BgmConfig; + estimated_duration: number; segments: Omit[]; } /** 使用模板生成请求体 */ export interface GenerateFromTemplatePayload { - voiceover_id: string; + voiceover_duration: number; } /** 使用模板生成响应 */ @@ -105,7 +122,7 @@ const MOCK_TEMPLATES: EditingTemplate[] = [ { id: 'tpl-1', name: '生活 Vlog 模板', - mode: '画中画', + mode: 'pip', category: '生活', tags: ['vlog', '日常'], title_config: { @@ -136,7 +153,7 @@ const MOCK_TEMPLATES: EditingTemplate[] = [ { id: 'tpl-2', name: '知识分享口播', - mode: '人物口播', + mode: 'voice_over', category: '知识', tags: ['口播', '分享'], title_config: { @@ -168,7 +185,7 @@ const MOCK_TEMPLATES: EditingTemplate[] = [ { id: 'tpl-3', name: '一镜到底展示', - mode: '一镜到底', + mode: 'one_take', category: '生活', tags: ['一镜到底'], title_config: { @@ -220,11 +237,6 @@ export const createEditingTemplate = async ( ): Promise => { await delay(300); const now = new Date().toISOString(); - const estimated = - data.segments.reduce( - (sum, s) => sum + (s.duration_min + s.duration_max) / 2, - 0, - ); const tpl: EditingTemplate = { id: nextId(), name: data.name, @@ -234,7 +246,7 @@ export const createEditingTemplate = async ( title_config: data.title_config, subtitle_config: data.subtitle_config, bgm_config: data.bgm_config, - estimated_duration: Math.round(estimated), + estimated_duration: data.estimated_duration, segments: data.segments.map((s, i) => ({ ...s, id: nextId(), @@ -255,11 +267,6 @@ export const updateEditingTemplate = async ( await delay(300); const idx = MOCK_TEMPLATES.findIndex((t) => t.id === id); if (idx === -1) throw new Error('模板不存在'); - const estimated = - data.segments.reduce( - (sum, s) => sum + (s.duration_min + s.duration_max) / 2, - 0, - ); const updated: EditingTemplate = { ...MOCK_TEMPLATES[idx], name: data.name, @@ -269,7 +276,7 @@ export const updateEditingTemplate = async ( title_config: data.title_config, subtitle_config: data.subtitle_config, bgm_config: data.bgm_config, - estimated_duration: Math.round(estimated), + estimated_duration: data.estimated_duration, segments: data.segments.map((s, i) => ({ ...s, id: nextId(), diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index fca9ca085..4dd1e7b8f 100644 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -2,44 +2,32 @@ * 剪辑计划编辑器 * 三栏布局:左侧模板面板 / 中间预览+时间线 / 右侧设置面板 * 支持 4 种模式切换(画中画 / 人物口播 / 一镜到底 / 口播+混剪) + * + * P0-2: 读取 URL 参数 ?template=xxx&generate=1 + * P1-3: 拆分为子组件 + * P1-4: voiceover_id → voiceover_duration + * P1-5: 分类 Input → Select(在 SaveModal 中实现) + * P1-6: SaveTemplatePayload 补充 estimated_duration */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import './EditingPlanner.css'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { - Button, - Typography, - Space, - Card, - Input, - Switch, - Select, - Slider, - Tag, - Empty, - Spin, - Modal, - message, -} from 'antd'; +import { Button, Space, message } from 'antd'; import { SaveOutlined, VideoCameraOutlined, - PlusOutlined, - DeleteOutlined, - DragOutlined, - PictureOutlined, - UserOutlined, AppstoreOutlined, - SoundOutlined, - FontSizeOutlined, + UserOutlined, DashboardOutlined, } from '@ant-design/icons'; +import { useSearchParams } from 'react-router-dom'; import { getEditingTemplates, getTemplateCategories, createEditingTemplate, updateEditingTemplate, generateFromTemplate, + MODE_LABELS, type EditingTemplate, type TemplateSegment, type TemplateMode, @@ -48,29 +36,20 @@ import { type BgmConfig, } from '@/api/editingPlanner'; -const { Text } = Typography; +/* ── 子组件 ── */ +import TemplatePanel from './components/TemplatePanel'; +import TimelinePanel from './components/TimelinePanel'; +import SettingsPanel from './components/SettingsPanel'; +import SaveModal from './components/SaveModal'; +import GenerateModal from './components/GenerateModal'; /* ──────────── 常量 ──────────── */ const MODES: { key: TemplateMode; icon: React.ReactNode; desc: string }[] = [ - { key: '画中画', icon: , desc: '多画面叠加' }, - { key: '人物口播', icon: , desc: '人物讲解为主' }, - { key: '一镜到底', icon: , desc: '连续不中断' }, - { key: '口播+混剪', icon: , desc: '口播搭配混剪素材' }, -]; - -const FONT_PRESETS = ['思源黑体', '站酷快乐体', '方正兰亭', '汉仪旗黑']; -const POSITIONS = [ - { value: 'top', label: '顶部' }, - { value: 'center', label: '居中' }, - { value: 'bottom', label: '底部' }, -]; -const SUBTITLE_FONTS = ['思源黑体', '微软雅黑', '苹方']; -const SUBTITLE_ANIMATIONS = [ - { value: 'none', label: '无' }, - { value: 'fade', label: '淡入' }, - { value: 'typewriter', label: '打字机' }, - { value: 'slide', label: '滑动' }, + { key: 'pip', icon: , desc: '多画面叠加' }, + { key: 'voice_over', icon: , desc: '人物讲解为主' }, + { key: 'one_take', icon: , desc: '连续不中断' }, + { key: 'voice_pip', icon: , desc: '口播搭配混剪素材' }, ]; const DEFAULT_TITLE: TitleConfig = { @@ -102,6 +81,11 @@ const newSegId = () => `seg-new-${++_segId}`; const EditingPlanner: React.FC = () => { const queryClient = useQueryClient(); + const [searchParams] = useSearchParams(); + + /* ── P0-2: URL 参数 ── */ + const urlTemplateId = searchParams.get('template'); + const urlGenerate = searchParams.get('generate'); /* ── 数据查询 ── */ const [searchText, setSearchText] = useState(''); @@ -122,7 +106,7 @@ const EditingPlanner: React.FC = () => { }); /* ── 编辑器状态 ── */ - const [currentMode, setCurrentMode] = useState('画中画'); + const [currentMode, setCurrentMode] = useState('pip'); const [segments, setSegments] = useState([ { id: newSegId(), segment_order: 1, duration_min: 5, duration_max: 15, material_type: null }, ]); @@ -138,9 +122,23 @@ const EditingPlanner: React.FC = () => { const [draftName, setDraftName] = useState(''); const [draftCategory, setDraftCategory] = useState(''); const [draftTags, setDraftTags] = useState(''); - const [voiceoverId, setVoiceoverId] = useState(''); + const [voiceoverDuration, setVoiceoverDuration] = useState(null); const [dragIdx, setDragIdx] = useState(null); + /* ── P0-2: 自动加载 URL 指定的模板 ── */ + useEffect(() => { + if (urlTemplateId && templates.length > 0 && !loadedTemplateId) { + const tpl = templates.find((t) => t.id === urlTemplateId); + if (tpl) { + loadTemplate(tpl); + // 如果 URL 有 generate=1,自动打开发成弹窗 + if (urlGenerate === '1') { + setGenerateModalOpen(true); + } + } + } + }, [urlTemplateId, templates, loadedTemplateId, urlGenerate]); + /* ── Mutations ── */ const createMutation = useMutation({ mutationFn: createEditingTemplate, @@ -167,8 +165,8 @@ const EditingPlanner: React.FC = () => { }); const generateMutation = useMutation({ - mutationFn: ({ templateId, voiceoverId }: { templateId: string; voiceoverId: string }) => - generateFromTemplate(templateId, { voiceover_id: voiceoverId }), + mutationFn: ({ templateId, duration }: { templateId: string; duration: number }) => + generateFromTemplate(templateId, { voiceover_duration: duration }), onSuccess: (data) => { const msg = data.warning ? `生成任务已提交(${data.warning})` : '生成任务已提交'; message.success(msg); @@ -184,7 +182,7 @@ const EditingPlanner: React.FC = () => { /* ──────────── 片段操作 ──────────── */ const addSegment = () => { - if (currentMode === '一镜到底') return; + if (currentMode === 'one_take') return; setSegments((prev) => [ ...prev, { @@ -192,13 +190,13 @@ const EditingPlanner: React.FC = () => { segment_order: prev.length + 1, duration_min: 5, duration_max: 15, - material_type: currentMode === '口播+混剪' ? '人物' : null, + material_type: currentMode === 'voice_pip' ? '人物' : null, }, ]); }; const removeSegment = (id: string) => { - if (currentMode === '一镜到底') return; + if (currentMode === 'one_take') return; setSegments((prev) => prev.filter((s) => s.id !== id).map((s, i) => ({ ...s, segment_order: i + 1 })), ); @@ -228,7 +226,7 @@ const EditingPlanner: React.FC = () => { const handleModeChange = (mode: TemplateMode) => { setCurrentMode(mode); - if (mode === '一镜到底') { + if (mode === 'one_take') { // 锁定为 1 个片段 setSegments([ { @@ -239,7 +237,7 @@ const EditingPlanner: React.FC = () => { material_type: null, }, ]); - } else if (mode === '口播+混剪') { + } else if (mode === 'voice_pip') { // 确保每个片段有 material_type setSegments((prev) => prev.map((s) => ({ @@ -263,7 +261,7 @@ const EditingPlanner: React.FC = () => { const resetEditor = () => { setLoadedTemplateId(null); - setCurrentMode('画中画'); + setCurrentMode('pip'); setSegments([ { id: newSegId(), segment_order: 1, duration_min: 5, duration_max: 15, material_type: null }, ]); @@ -292,6 +290,7 @@ const EditingPlanner: React.FC = () => { message.warning('请输入模板名称'); return; } + const estimatedDuration = calcEstimatedDuration(segments); const payload = { name: draftName.trim(), mode: currentMode, @@ -303,6 +302,7 @@ const EditingPlanner: React.FC = () => { title_config: titleConfig, subtitle_config: subtitleConfig, bgm_config: bgmConfig, + estimated_duration: estimatedDuration, segments: segments.map(({ id: _id, ...rest }) => rest), }; @@ -322,16 +322,14 @@ const EditingPlanner: React.FC = () => { }; const doGenerate = () => { - if (!voiceoverId.trim()) { - message.warning('请输入配音 ID'); + if (!voiceoverDuration || voiceoverDuration <= 0) { + message.warning('请输入配音时长'); return; } - generateMutation.mutate({ templateId: loadedTemplateId!, voiceoverId: voiceoverId.trim() }); + generateMutation.mutate({ templateId: loadedTemplateId!, duration: voiceoverDuration }); }; const estimatedDuration = calcEstimatedDuration(segments); - const isOneShot = currentMode === '一镜到底'; - const isMixedCut = currentMode === '口播+混剪'; /* ──────────── 渲染 ──────────── */ @@ -347,7 +345,7 @@ const EditingPlanner: React.FC = () => { icon={m.icon} onClick={() => handleModeChange(m.key)} > - {m.key} + {MODE_LABELS[m.key]} ))} @@ -368,479 +366,71 @@ const EditingPlanner: React.FC = () => { {/* ═══ 三栏主体 ═══ */}
- {/* ─── 左侧:模板面板 ─── */} -
-
- - 我的模板 - - setSearchText(e.target.value)} - allowClear - size="small" - style={{ marginBottom: 8 }} - /> - updateSegment(seg.id, { material_type: v })} - style={{ width: '100%', marginTop: 4 }} - options={[ - { value: '人物', label: '人物' }, - { value: '场景', label: '场景' }, - ]} - /> -
- )} - - ))} -
-
- - - {/* ─── 右侧:设置面板 ─── */} -
- {/* 标题设置 */} -
- - - 标题设置 - - -
- AI 自动选择 - setTitleConfig((prev) => ({ ...prev, ai_auto_select: checked }))} - checkedChildren="ON" - unCheckedChildren="OFF" - /> -
- - {!titleConfig.ai_auto_select && ( - setTitleConfig((prev) => ({ ...prev, content: e.target.value }))} - rows={2} - size="small" - style={{ marginBottom: 12 }} - /> - )} - -
- 字体预设 -
- {FONT_PRESETS.map((font) => ( - setTitleConfig((prev) => ({ ...prev, font_preset: font }))} - > - {font} - - ))} -
-
- -
-
- 颜色 - setTitleConfig((prev) => ({ ...prev, font_color: e.target.value }))} - style={{ marginTop: 4 }} - /> -
-
- 位置 - setSubtitleConfig((prev) => ({ ...prev, position: v }))} - options={POSITIONS} - style={{ width: '100%', marginTop: 4 }} - /> -
-
- 字体 - setSubtitleConfig((prev) => ({ ...prev, color: e.target.value }))} - style={{ marginTop: 4 }} - /> -
-
- 动画 - setBgmConfig((prev) => ({ ...prev, music_id: v }))} - style={{ width: '100%', marginTop: 4 }} - options={[ - { value: 'bgm-1', label: '轻快节奏' }, - { value: 'bgm-2', label: '舒缓氛围' }, - { value: 'bgm-3', label: '动感活力' }, - ]} - /> -
- )} -
-
+ {/* 右侧:设置面板 */} +
- {/* ═══ 保存模板弹窗 ═══ */} - setSaveModalOpen(false)} - onOk={handleSave} - confirmLoading={saving} - okText="保存" - > - -
- 模板名称 * - setDraftName(e.target.value)} - /> -
-
- 分类 - setDraftCategory(e.target.value)} - /> -
-
- 标签(逗号分隔) - setDraftTags(e.target.value)} - /> -
-
-
+ /> - {/* ═══ 使用模板生成弹窗 ═══ */} - setGenerateModalOpen(false)} - onOk={doGenerate} - confirmLoading={generateMutation.isPending} - okText="开始生成" - > - -
- 配音 ID * - setVoiceoverId(e.target.value)} - /> -
- {loadedTemplateId && ( - - 预估时长:~{estimatedDuration}s,配音时长偏差超过 ±30% 时将收到警告 - - )} -
-
+ /> ); }; diff --git a/apps/web/src/pages/editing-planner/components/GenerateModal.tsx b/apps/web/src/pages/editing-planner/components/GenerateModal.tsx new file mode 100644 index 000000000..6fd02beb2 --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/GenerateModal.tsx @@ -0,0 +1,58 @@ +/** + * 使用模板生成视频弹窗 + * P1-4: voiceover_id → voiceover_duration (number) + */ +import React from 'react'; +import { Modal, InputNumber, Space, Typography } from 'antd'; + +const { Text } = Typography; + +interface GenerateModalProps { + open: boolean; + loading: boolean; + voiceoverDuration: number | null; + estimatedDuration: number; + onDurationChange: (v: number | null) => void; + onGenerate: () => void; + onCancel: () => void; +} + +const GenerateModal: React.FC = ({ + open, + loading, + voiceoverDuration, + estimatedDuration, + onDurationChange, + onGenerate, + onCancel, +}) => { + return ( + + +
+ 配音时长(秒)* + +
+ + 预估时长:~{estimatedDuration}s,配音时长偏差超过 ±30% 时将收到警告 + +
+
+ ); +}; + +export default GenerateModal; diff --git a/apps/web/src/pages/editing-planner/components/SaveModal.tsx b/apps/web/src/pages/editing-planner/components/SaveModal.tsx new file mode 100644 index 000000000..aa60d2c81 --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/SaveModal.tsx @@ -0,0 +1,88 @@ +/** + * 保存/更新模板弹窗 + * 分类使用 Select 关联后端分类 API(P1-5) + */ +import React from 'react'; +import { Modal, Input, Select, Space, Typography } from 'antd'; +import type { TemplateCategory } from '@/api/editingPlanner'; + +const { Text } = Typography; + +interface SaveModalProps { + open: boolean; + loading: boolean; + isUpdate: boolean; + draftName: string; + draftCategory: string; + draftTags: string; + categories: TemplateCategory[]; + estimatedDuration: number; + onNameChange: (v: string) => void; + onCategoryChange: (v: string) => void; + onTagsChange: (v: string) => void; + onSave: () => void; + onCancel: () => void; +} + +const SaveModal: React.FC = ({ + open, + loading, + isUpdate, + draftName, + draftCategory, + draftTags, + categories, + estimatedDuration, + onNameChange, + onCategoryChange, + onTagsChange, + onSave, + onCancel, +}) => { + return ( + + +
+ 模板名称 * + onNameChange(e.target.value)} + /> +
+
+ 分类 + onTagsChange(e.target.value)} + /> +
+ + 预估时长:~{estimatedDuration}s + +
+
+ ); +}; + +export default SaveModal; diff --git a/apps/web/src/pages/editing-planner/components/SettingsPanel.tsx b/apps/web/src/pages/editing-planner/components/SettingsPanel.tsx new file mode 100644 index 000000000..2c1da27d2 --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/SettingsPanel.tsx @@ -0,0 +1,229 @@ +/** + * 右侧设置面板 + * 标题设置 / 字幕设置 / BGM 设置 + */ +import React from 'react'; +import { Typography, Input, Switch, Select, Slider, Tag } from 'antd'; +import { SoundOutlined, FontSizeOutlined } from '@ant-design/icons'; +import type { TitleConfig, SubtitleConfig, BgmConfig } from '@/api/editingPlanner'; + +const { Text } = Typography; + +/* ── 常量 ── */ +const FONT_PRESETS = ['思源黑体', '站酷快乐体', '方正兰亭', '汉仪旗黑']; +const POSITIONS = [ + { value: 'top', label: '顶部' }, + { value: 'center', label: '居中' }, + { value: 'bottom', label: '底部' }, +]; +const SUBTITLE_FONTS = ['思源黑体', '微软雅黑', '苹方']; +const SUBTITLE_ANIMATIONS = [ + { value: 'none', label: '无' }, + { value: 'fade', label: '淡入' }, + { value: 'typewriter', label: '打字机' }, + { value: 'slide', label: '滑动' }, +]; + +interface SettingsPanelProps { + titleConfig: TitleConfig; + subtitleConfig: SubtitleConfig; + bgmConfig: BgmConfig; + onTitleChange: (config: TitleConfig) => void; + onSubtitleChange: (config: SubtitleConfig) => void; + onBgmChange: (config: BgmConfig) => void; +} + +const SettingsPanel: React.FC = ({ + titleConfig, + subtitleConfig, + bgmConfig, + onTitleChange, + onSubtitleChange, + onBgmChange, +}) => { + return ( +
+ {/* 标题设置 */} +
+ + + 标题设置 + + +
+ AI 自动选择 + onTitleChange({ ...titleConfig, ai_auto_select: checked })} + checkedChildren="ON" + unCheckedChildren="OFF" + /> +
+ + {!titleConfig.ai_auto_select && ( + onTitleChange({ ...titleConfig, content: e.target.value })} + rows={2} + size="small" + style={{ marginBottom: 12 }} + /> + )} + +
+ 字体预设 +
+ {FONT_PRESETS.map((font) => ( + onTitleChange({ ...titleConfig, font_preset: font })} + > + {font} + + ))} +
+
+ +
+
+ 颜色 + onTitleChange({ ...titleConfig, font_color: e.target.value })} + style={{ marginTop: 4 }} + /> +
+
+ 位置 + onSubtitleChange({ ...subtitleConfig, position: v })} + options={POSITIONS} + style={{ width: '100%', marginTop: 4 }} + /> +
+
+ 字体 + onSubtitleChange({ ...subtitleConfig, color: e.target.value })} + style={{ marginTop: 4 }} + /> +
+
+ 动画 + onBgmChange({ ...bgmConfig, music_id: v })} + style={{ width: '100%', marginTop: 4 }} + options={[ + { value: 'bgm-1', label: '轻快节奏' }, + { value: 'bgm-2', label: '舒缓氛围' }, + { value: 'bgm-3', label: '动感活力' }, + ]} + /> +
+ )} +
+
+ ); +}; + +export default SettingsPanel; diff --git a/apps/web/src/pages/editing-planner/components/TemplatePanel.tsx b/apps/web/src/pages/editing-planner/components/TemplatePanel.tsx new file mode 100644 index 000000000..91be782a1 --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/TemplatePanel.tsx @@ -0,0 +1,130 @@ +/** + * 左侧模板面板 + * 搜索、分类筛选、模板卡片列表 + */ +import React from 'react'; +import { Input, Select, Card, Tag, Empty, Spin, Button, Typography } from 'antd'; +import { + SearchOutlined, +} from '@ant-design/icons'; +import { + MODE_LABELS, + MODE_COLORS, + type EditingTemplate, + type TemplateCategory, + type TemplateMode, +} from '@/api/editingPlanner'; + +const { Text } = Typography; + +interface TemplatePanelProps { + templates: EditingTemplate[]; + categories: TemplateCategory[]; + isLoading: boolean; + searchText: string; + filterCategory: string; + loadedTemplateId: string | null; + onSearchChange: (v: string) => void; + onCategoryChange: (v: string) => void; + onTemplateSelect: (tpl: EditingTemplate) => void; + onNewTemplate: () => void; +} + +const TemplatePanel: React.FC = ({ + templates, + categories, + isLoading, + searchText, + filterCategory, + loadedTemplateId, + onSearchChange, + onCategoryChange, + onTemplateSelect, + onNewTemplate, +}) => { + return ( +
+
+ + 我的模板 + + } + placeholder="搜索模板..." + value={searchText} + onChange={(e) => onSearchChange(e.target.value)} + allowClear + size="small" + style={{ marginBottom: 8 }} + /> + onUpdateSegment(seg.id, { material_type: v })} + style={{ width: '100%', marginTop: 4 }} + options={[ + { value: '人物', label: '人物' }, + { value: '场景', label: '场景' }, + ]} + /> +
+ )} + + ))} +
+
+ + ); +}; + +export default TimelinePanel; diff --git a/apps/web/src/pages/my-templates/MyTemplates.tsx b/apps/web/src/pages/my-templates/MyTemplates.tsx index 9bcf5f10e..c1a8b16d7 100644 --- a/apps/web/src/pages/my-templates/MyTemplates.tsx +++ b/apps/web/src/pages/my-templates/MyTemplates.tsx @@ -36,7 +36,10 @@ import { getTemplateCategories, deleteEditingTemplate, createEditingTemplate, + MODE_LABELS, + MODE_COLORS, type EditingTemplate, + type TemplateMode, } from '@/api/editingPlanner'; import './MyTemplates.css'; @@ -86,6 +89,7 @@ const MyTemplates: React.FC = () => { title_config: tpl.title_config, subtitle_config: tpl.subtitle_config, bgm_config: tpl.bgm_config, + estimated_duration: tpl.estimated_duration ?? Math.round(tpl.segments.reduce((s, seg) => s + (seg.duration_min + seg.duration_max) / 2, 0)), segments: tpl.segments.map(({ id: _id, ...rest }) => rest), }), onSuccess: () => { @@ -114,14 +118,6 @@ const MyTemplates: React.FC = () => { deleteMutation.mutate(id); }; - /* ── 模式颜色 ── */ - const modeColor: Record = { - 画中画: 'blue', - 人物口播: 'green', - 一镜到底: 'orange', - '口播+混剪': 'purple', - }; - return (
{/* 页面头部 */} @@ -211,7 +207,7 @@ const MyTemplates: React.FC = () => { {tpl.name} - {tpl.mode} + {MODE_LABELS[tpl.mode as TemplateMode] || tpl.mode}