From 8c4c428cdcc5086c8fd23c7cfdbacbd150d25d12 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 29 Jun 2026 16:33:23 +0800 Subject: [PATCH 01/12] =?UTF-8?q?fix(api):=20editingPlanner=20=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5=E5=90=8E=E7=AB=AF=20/templates=20API=EF=BC=8C?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=81=97=E7=95=99=20editPlans.ts=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squash merge PR#108 - editingPlanner.ts: mock → 真实 /templates API(7个函数) - editPlans.ts: 已删除 - 代码审计通过 --- apps/web/src/api/editPlans.ts | 98 ------------- apps/web/src/api/editingPlanner.ts | 224 +++++++---------------------- 2 files changed, 52 insertions(+), 270 deletions(-) delete mode 100644 apps/web/src/api/editPlans.ts diff --git a/apps/web/src/api/editPlans.ts b/apps/web/src/api/editPlans.ts deleted file mode 100644 index 50f98674d..000000000 --- a/apps/web/src/api/editPlans.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * 编辑计划 API - * Phase 1 重构:去掉 projectId,编辑计划直接归属用户 - */ -import apiClient from './client'; - -/** 编辑模式 */ -export type EditingMode = 'one-take' | 'pip' | 'voiceover' | 'voice_pip'; - -/** 编辑模板 */ -export interface EditTemplateItem { - id: string; - name: string; - description: string; - target_duration: number; - clip_count: number; - is_active: boolean; - category?: string; - thumbnail_url?: string; -} - -/** 编辑计划片段 */ -export interface EditPlanClipItem { - id: string; - asset_id: string; - asset_name: string; - sequence: number; - start_time: number; - duration: number; - reason: string; - layer?: 'main' | 'pip' | 'broll'; - thumbnail_url?: string; -} - -/** 编辑计划 */ -export interface EditPlanItem { - id: string; - template_id: string; - asset_library_id: string; - title_id: string; - status: string; - editing_mode?: EditingMode; - summary: string; - clips: EditPlanClipItem[]; - created_at?: string; - updated_at?: string; -} - -// ─── 编辑计划 ────────────────────────────────────────────── - -/** 获取当前用户的编辑计划列表 */ -export const getEditPlans = async (): Promise => { - const response = await apiClient.get('/edit-plans'); - return response.data.items || []; -}; - -/** 获取单个编辑计划 */ -export const getEditPlan = async (planId: string): Promise => { - const response = await apiClient.get(`/edit-plans/${planId}`); - return response.data; -}; - -/** 创建编辑计划 */ -export const createEditPlan = async (data: { - asset_library_id: string; - template_id?: string; - title_id?: string; -}): Promise => { - const response = await apiClient.post('/edit-plans', data); - return response.data; -}; - -/** 智能编排 - 自动生成编辑计划 */ -export const autoGenerateEditPlan = async (params: { - template_id: string; - asset_ids?: string[]; - title_ids?: string[]; - voice_ids?: string[]; - editing_mode?: EditingMode; - target_duration?: number; -}): Promise => { - const response = await apiClient.post('/edit-plans/auto-generate', params); - return response.data; -}; - -/** 更新编辑计划 */ -export const updateEditPlan = async ( - planId: string, - data: Partial -): Promise => { - const response = await apiClient.patch(`/edit-plans/${planId}`, data); - return response.data; -}; - -/** 删除编辑计划 */ -export const deleteEditPlan = async (planId: string): Promise => { - await apiClient.delete(`/edit-plans/${planId}`); -}; diff --git a/apps/web/src/api/editingPlanner.ts b/apps/web/src/api/editingPlanner.ts index 063acbc9f..9c032d66a 100644 --- a/apps/web/src/api/editingPlanner.ts +++ b/apps/web/src/api/editingPlanner.ts @@ -1,8 +1,8 @@ /** * 剪辑计划编辑器 API - * 当前使用 mock 数据,后端 API 就绪后替换 + * 对接后端 /api/v1/templates 路由 */ -// import apiClient from './client'; // TODO: 后端 API 就绪后启用 +import apiClient from './client'; /* ──────────── 类型定义 ──────────── */ @@ -53,11 +53,11 @@ export interface BgmConfig { /** 模板片段 */ export interface TemplateSegment { - id: string; + id?: string; segment_order: number; duration_min: number; duration_max: number; - material_type: string | null; // 仅 口播+混剪 模式:人物/场景 + material_type: string | null; } /** 剪辑模板 */ @@ -72,6 +72,7 @@ export interface EditingTemplate { bgm_config: BgmConfig; estimated_duration: number; segments: TemplateSegment[]; + is_active?: boolean; created_at: string; updated_at: string; } @@ -80,6 +81,7 @@ export interface EditingTemplate { export interface TemplateCategory { id: string; name: string; + created_at?: string; } /** 创建/更新模板请求体 */ @@ -100,124 +102,46 @@ export interface GenerateFromTemplatePayload { voiceover_duration: number; } -/** 使用模板生成响应 */ -export interface GenerateFromTemplateResponse { - task_id: string; - warning?: string; +/** 验证/生成响应 */ +export interface ValidateWarning { + code: string; + message: string; + details?: Record; } -/* ──────────── Mock 数据 ──────────── */ +/** 使用模板生成响应 */ +export interface GenerateFromTemplateResponse { + template: EditingTemplate; + warnings: ValidateWarning[]; +} -let _nextId = 100; -const nextId = () => String(++_nextId); +/** 列表响应(带分页) */ +export interface ListTemplatesResponse { + items: EditingTemplate[]; + total: number; +} -const MOCK_CATEGORIES: TemplateCategory[] = [ - { id: 'cat-1', name: '生活' }, - { id: 'cat-2', name: '美食' }, - { id: 'cat-3', name: '旅行' }, - { id: 'cat-4', name: '知识' }, -]; +/** 分类列表响应 */ +export interface ListCategoriesResponse { + items: TemplateCategory[]; +} -const MOCK_TEMPLATES: EditingTemplate[] = [ - { - id: 'tpl-1', - name: '生活 Vlog 模板', - mode: 'pip', - category: '生活', - tags: ['vlog', '日常'], - title_config: { - ai_auto_select: true, - content: '', - font_preset: '思源黑体', - font_color: '#ffffff', - font_size: 32, - position: 'top', - }, - subtitle_config: { - enabled: true, - position: 'bottom', - font: '思源黑体', - color: '#ffffff', - size: 24, - animation: 'fade', - }, - bgm_config: { enabled: true, music_id: 'bgm-1' }, - estimated_duration: 30, - segments: [ - { id: 'seg-1', segment_order: 1, duration_min: 5, duration_max: 15, material_type: null }, - { id: 'seg-2', segment_order: 2, duration_min: 10, duration_max: 20, material_type: null }, - ], - created_at: '2026-06-20T10:00:00Z', - updated_at: '2026-06-20T10:00:00Z', - }, - { - id: 'tpl-2', - name: '知识分享口播', - mode: 'voice_over', - category: '知识', - tags: ['口播', '分享'], - title_config: { - ai_auto_select: false, - content: '每日知识分享', - font_preset: '站酷快乐体', - font_color: '#ffdd00', - font_size: 36, - position: 'top', - }, - subtitle_config: { - enabled: true, - position: 'bottom', - font: '思源黑体', - color: '#ffffff', - size: 28, - animation: 'typewriter', - }, - bgm_config: { enabled: false, music_id: '' }, - estimated_duration: 60, - segments: [ - { id: 'seg-3', segment_order: 1, duration_min: 10, duration_max: 30, material_type: null }, - { id: 'seg-4', segment_order: 2, duration_min: 20, duration_max: 40, material_type: null }, - { id: 'seg-5', segment_order: 3, duration_min: 10, duration_max: 20, material_type: null }, - ], - created_at: '2026-06-21T10:00:00Z', - updated_at: '2026-06-21T10:00:00Z', - }, - { - id: 'tpl-3', - name: '一镜到底展示', - mode: 'one_take', - category: '生活', - tags: ['一镜到底'], - title_config: { - ai_auto_select: true, - content: '', - font_preset: '思源黑体', - font_color: '#ffffff', - font_size: 32, - position: 'center', - }, - subtitle_config: { enabled: false, position: 'bottom', font: '思源黑体', color: '#ffffff', size: 24, animation: 'fade' }, - bgm_config: { enabled: true, music_id: 'bgm-2' }, - estimated_duration: 15, - segments: [ - { id: 'seg-6', segment_order: 1, duration_min: 10, duration_max: 20, material_type: null }, - ], - created_at: '2026-06-22T10:00:00Z', - updated_at: '2026-06-22T10:00:00Z', - }, -]; - -/* ──────────── Mock API 函数 ──────────── */ - -const delay = (ms = 200) => new Promise((r) => setTimeout(r, ms)); +// ============ API 函数 ============ /** 获取模板列表 */ export const getEditingTemplates = async (params?: { category?: string; tag?: string; + skip?: number; + limit?: number; }): Promise => { - await delay(); - let list = [...MOCK_TEMPLATES]; + const response = await apiClient.get('/templates', { + params: { + skip: params?.skip ?? 0, + limit: params?.limit ?? 50, + }, + }); + let list = response.data.items; if (params?.category) list = list.filter((t) => t.category === params.category); if (params?.tag) list = list.filter((t) => t.tags.includes(params.tag!)); return list; @@ -225,38 +149,16 @@ export const getEditingTemplates = async (params?: { /** 获取模板详情 */ export const getEditingTemplate = async (id: string): Promise => { - await delay(); - const tpl = MOCK_TEMPLATES.find((t) => t.id === id); - if (!tpl) throw new Error('模板不存在'); - return { ...tpl }; + const response = await apiClient.get(`/templates/${id}`); + return response.data; }; /** 创建模板 */ export const createEditingTemplate = async ( data: SaveTemplatePayload, ): Promise => { - await delay(300); - const now = new Date().toISOString(); - const tpl: EditingTemplate = { - id: nextId(), - name: data.name, - mode: data.mode, - category: data.category, - tags: data.tags, - title_config: data.title_config, - subtitle_config: data.subtitle_config, - bgm_config: data.bgm_config, - estimated_duration: data.estimated_duration, - segments: data.segments.map((s, i) => ({ - ...s, - id: nextId(), - segment_order: i + 1, - })), - created_at: now, - updated_at: now, - }; - MOCK_TEMPLATES.push(tpl); - return tpl; + const response = await apiClient.post('/templates', data); + return response.data; }; /** 更新模板 */ @@ -264,51 +166,29 @@ export const updateEditingTemplate = async ( id: string, data: SaveTemplatePayload, ): Promise => { - await delay(300); - const idx = MOCK_TEMPLATES.findIndex((t) => t.id === id); - if (idx === -1) throw new Error('模板不存在'); - const updated: EditingTemplate = { - ...MOCK_TEMPLATES[idx], - name: data.name, - mode: data.mode, - category: data.category, - tags: data.tags, - title_config: data.title_config, - subtitle_config: data.subtitle_config, - bgm_config: data.bgm_config, - estimated_duration: data.estimated_duration, - segments: data.segments.map((s, i) => ({ - ...s, - id: nextId(), - segment_order: i + 1, - })), - updated_at: new Date().toISOString(), - }; - MOCK_TEMPLATES[idx] = updated; - return updated; + const response = await apiClient.patch(`/templates/${id}`, data); + return response.data; }; /** 删除模板 */ export const deleteEditingTemplate = async (id: string): Promise => { - await delay(); - const idx = MOCK_TEMPLATES.findIndex((t) => t.id === id); - if (idx !== -1) MOCK_TEMPLATES.splice(idx, 1); + await apiClient.delete(`/templates/${id}`); }; /** 获取模板分类列表 */ export const getTemplateCategories = async (): Promise => { - await delay(); - return [...MOCK_CATEGORIES]; + const response = await apiClient.get('/templates/categories/list'); + return response.data.items; }; -/** 使用模板生成视频 */ +/** 使用模板生成视频(调用 validate 端点) */ export const generateFromTemplate = async ( - _templateId: string, - _data: GenerateFromTemplatePayload, + templateId: string, + data: GenerateFromTemplatePayload, ): Promise => { - await delay(500); - return { - task_id: nextId(), - warning: undefined, - }; + const response = await apiClient.post( + `/templates/${templateId}/validate`, + data, + ); + return response.data; }; From 8c678b9cd08591018db9eda6d18ce746c3bce6f6 Mon Sep 17 00:00:00 2001 From: CI Test Date: Mon, 29 Jun 2026 16:40:06 +0800 Subject: [PATCH 02/12] =?UTF-8?q?fix:=20TypeScript=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E4=BF=AE=E5=A4=8D=20-=20warning=E2=86=92warn?= =?UTF-8?q?ings,=20seg.id=E9=9D=9E=E7=A9=BA=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/editing-planner/EditingPlanner.tsx | 2 +- .../pages/editing-planner/components/TimelinePanel.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index 4dd1e7b8f..e38d2df1d 100644 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -168,7 +168,7 @@ const EditingPlanner: React.FC = () => { mutationFn: ({ templateId, duration }: { templateId: string; duration: number }) => generateFromTemplate(templateId, { voiceover_duration: duration }), onSuccess: (data) => { - const msg = data.warning ? `生成任务已提交(${data.warning})` : '生成任务已提交'; + const msg = data.warnings && data.warnings.length > 0 ? `生成任务已提交(${data.warnings.map(w => w.message).join('; ')})` : '生成任务已提交'; message.success(msg); setGenerateModalOpen(false); }, diff --git a/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx b/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx index 6f1d0cbb1..709be93c7 100644 --- a/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx +++ b/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx @@ -136,7 +136,7 @@ const TimelinePanel: React.FC = ({ size="small" danger icon={} - onClick={() => onRemoveSegment(seg.id)} + onClick={() => onRemoveSegment(seg.id!)} disabled={isOneShot} style={{ marginLeft: 'auto' }} /> @@ -154,7 +154,7 @@ const TimelinePanel: React.FC = ({ min={1} max={seg.duration_max} value={seg.duration_min} - onChange={(v) => onUpdateSegment(seg.id, { duration_min: v })} + onChange={(v) => onUpdateSegment(seg.id!, { duration_min: v })} />
@@ -163,7 +163,7 @@ const TimelinePanel: React.FC = ({ min={seg.duration_min} max={60} value={seg.duration_max} - onChange={(v) => onUpdateSegment(seg.id, { duration_max: v })} + onChange={(v) => onUpdateSegment(seg.id!, { duration_max: v })} />
@@ -175,7 +175,7 @@ const TimelinePanel: React.FC = ({