feat(task-2.14): 剪辑计划编辑器完整版 — 三栏布局 + 拖拽 + 素材面板
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 189h47m20s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 189h47m22s
Deploy / Deploy Staging (push) Failing after 189h47m51s
CI/CD Pipeline / Frontend Lint (push) Failing after 189h48m17s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 189h48m22s
Deploy / Production Browser E2E (push) Failing after 1848h22m59s
Deploy / Deploy Production (push) Failing after 1848h23m2s
Deploy / Build Production Runtime Images (push) Failing after 1848h23m33s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 189h47m20s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 189h47m22s
Deploy / Deploy Staging (push) Failing after 189h47m51s
CI/CD Pipeline / Frontend Lint (push) Failing after 189h48m17s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 189h48m22s
Deploy / Production Browser E2E (push) Failing after 1848h22m59s
Deploy / Deploy Production (push) Failing after 1848h23m2s
Deploy / Build Production Runtime Images (push) Failing after 1848h23m33s
- 新增 editPlans API 模块(Mock 数据,预留真实接口) - 新增 MediaPanel 组件:左侧 Tab 切换(模板+素材),素材可拖拽 - 重写 TimelinePanel:可视化时长条 + 片段卡片 + 拖拽排序 + 素材接收 - 新增 ClipPropertiesPanel:右侧片段属性编辑(文案/时长/转场/素材关联) - 升级 EditingPlanner 主页面:集成三栏布局 + 剪辑计划 CRUD - 重写 EditingPlanner.css:全新 V21 设计系统样式 + 响应式适配 - 修复 TypeScript 类型错误 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* 剪辑计划 API — Mock 数据 + 预留接口
|
||||
* 任务 2.14:对接后端 GET/POST/PUT /api/v1/edit-plans
|
||||
* 当前使用 Mock 数据,后续替换为真实 API 调用
|
||||
*/
|
||||
import type { TemplateMode } from "./editingPlanner";
|
||||
|
||||
/* ============================================================
|
||||
* 类型定义
|
||||
* ============================================================ */
|
||||
|
||||
/** 剪辑计划中的片段 */
|
||||
export interface EditPlanClip {
|
||||
id: string;
|
||||
template_segment_id: string;
|
||||
/** 素材库中的素材 ID */
|
||||
media_asset_id?: string;
|
||||
/** 素材类型 */
|
||||
material_type: "video" | "image" | "audio" | "voiceover";
|
||||
/** 片段文案 */
|
||||
script_text: string;
|
||||
/** 实际时长(秒) */
|
||||
duration: number;
|
||||
/** 转场效果 */
|
||||
transition?: TransitionEffect;
|
||||
/** 排序 */
|
||||
order: number;
|
||||
}
|
||||
|
||||
/** 转场效果 */
|
||||
export interface TransitionEffect {
|
||||
type: "none" | "fade" | "dissolve" | "wipe" | "zoom" | "slide";
|
||||
duration: number; // 转场时长(秒)
|
||||
}
|
||||
|
||||
/** 剪辑计划 */
|
||||
export interface EditPlan {
|
||||
id: string;
|
||||
name: string;
|
||||
template_id?: string;
|
||||
mode: TemplateMode;
|
||||
clips: EditPlanClip[];
|
||||
/** 总预估时长 */
|
||||
total_duration: number;
|
||||
/** 状态 */
|
||||
status: "draft" | "ready" | "generating" | "completed" | "failed";
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** 创建剪辑计划请求 */
|
||||
export interface CreateEditPlanRequest {
|
||||
name: string;
|
||||
template_id?: string;
|
||||
mode: TemplateMode;
|
||||
clips: Omit<EditPlanClip, "id">[];
|
||||
}
|
||||
|
||||
/** 更新剪辑计划请求 */
|
||||
export interface UpdateEditPlanRequest {
|
||||
name?: string;
|
||||
clips?: EditPlanClip[];
|
||||
status?: EditPlan["status"];
|
||||
}
|
||||
|
||||
/** 素材库资产 */
|
||||
export interface MediaAsset {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "video" | "image" | "audio";
|
||||
/** 缩略图 URL */
|
||||
thumbnail_url?: string;
|
||||
/** 时长(秒),仅 video/audio */
|
||||
duration?: number;
|
||||
/** 文件大小(字节) */
|
||||
size?: number;
|
||||
/** 标签 */
|
||||
tags: string[];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Mock 数据
|
||||
* ============================================================ */
|
||||
|
||||
const MOCK_ASSETS: MediaAsset[] = [
|
||||
{
|
||||
id: "asset-001",
|
||||
name: "产品展示-正面.mp4",
|
||||
type: "video",
|
||||
duration: 15,
|
||||
size: 12_500_000,
|
||||
tags: ["产品", "展示"],
|
||||
created_at: "2026-06-20T10:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "asset-002",
|
||||
name: "使用教程-片段A.mp4",
|
||||
type: "video",
|
||||
duration: 20,
|
||||
size: 18_000_000,
|
||||
tags: ["教程", "使用"],
|
||||
created_at: "2026-06-21T14:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "asset-003",
|
||||
name: "背景音乐-轻快.mp3",
|
||||
type: "audio",
|
||||
duration: 120,
|
||||
size: 3_200_000,
|
||||
tags: ["BGM", "轻快"],
|
||||
created_at: "2026-06-18T09:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "asset-004",
|
||||
name: "封面图-主图.jpg",
|
||||
type: "image",
|
||||
size: 850_000,
|
||||
tags: ["封面", "主图"],
|
||||
created_at: "2026-06-22T11:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "asset-005",
|
||||
name: "细节特写-侧面.mp4",
|
||||
type: "video",
|
||||
duration: 10,
|
||||
size: 8_500_000,
|
||||
tags: ["产品", "细节"],
|
||||
created_at: "2026-06-23T16:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "asset-006",
|
||||
name: "开箱视频-片段.mp4",
|
||||
type: "video",
|
||||
duration: 25,
|
||||
size: 22_000_000,
|
||||
tags: ["开箱", "展示"],
|
||||
created_at: "2026-06-19T08:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "asset-007",
|
||||
name: "配音-产品介绍.wav",
|
||||
type: "audio",
|
||||
duration: 45,
|
||||
size: 5_600_000,
|
||||
tags: ["配音", "产品"],
|
||||
created_at: "2026-06-24T10:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "asset-008",
|
||||
name: "场景图-生活场景.jpg",
|
||||
type: "image",
|
||||
size: 1_200_000,
|
||||
tags: ["场景", "生活"],
|
||||
created_at: "2026-06-25T13:00:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
let mockPlans: EditPlan[] = [
|
||||
{
|
||||
id: "plan-001",
|
||||
name: "产品展示视频 v1",
|
||||
template_id: "tpl-001",
|
||||
mode: "pip",
|
||||
clips: [
|
||||
{
|
||||
id: "clip-001",
|
||||
template_segment_id: "seg-001",
|
||||
media_asset_id: "asset-001",
|
||||
material_type: "video",
|
||||
script_text: "大家好,今天给大家带来一款超值好物!",
|
||||
duration: 10,
|
||||
transition: { type: "fade", duration: 0.5 },
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
id: "clip-002",
|
||||
template_segment_id: "seg-002",
|
||||
media_asset_id: "asset-005",
|
||||
material_type: "video",
|
||||
script_text: "来看看这个细节做工,真的绝了",
|
||||
duration: 8,
|
||||
transition: { type: "dissolve", duration: 0.3 },
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
id: "clip-003",
|
||||
template_segment_id: "seg-003",
|
||||
material_type: "image",
|
||||
script_text: "多种颜色可选,总有一款适合你",
|
||||
duration: 6,
|
||||
transition: { type: "none", duration: 0 },
|
||||
order: 2,
|
||||
},
|
||||
],
|
||||
total_duration: 24,
|
||||
status: "draft",
|
||||
created_at: "2026-06-25T10:00:00Z",
|
||||
updated_at: "2026-06-25T15:00:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
/* ============================================================
|
||||
* Mock 延迟
|
||||
* ============================================================ */
|
||||
const delay = (ms = 300) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
/* ============================================================
|
||||
* API 函数(Mock 实现,后续替换为真实 API)
|
||||
* ============================================================ */
|
||||
|
||||
/** 获取剪辑计划列表 */
|
||||
export async function getEditPlans(): Promise<EditPlan[]> {
|
||||
// TODO: 替换为 apiClient.get('/edit-plans')
|
||||
await delay();
|
||||
return [...mockPlans];
|
||||
}
|
||||
|
||||
/** 获取单个剪辑计划 */
|
||||
export async function getEditPlan(id: string): Promise<EditPlan> {
|
||||
// TODO: 替换为 apiClient.get(`/edit-plans/${id}`)
|
||||
await delay();
|
||||
const plan = mockPlans.find((p) => p.id === id);
|
||||
if (!plan) throw new Error(`剪辑计划 ${id} 不存在`);
|
||||
return { ...plan };
|
||||
}
|
||||
|
||||
/** 创建剪辑计划 */
|
||||
export async function createEditPlan(
|
||||
data: CreateEditPlanRequest
|
||||
): Promise<EditPlan> {
|
||||
// TODO: 替换为 apiClient.post('/edit-plans', data)
|
||||
await delay();
|
||||
const now = new Date().toISOString();
|
||||
const plan: EditPlan = {
|
||||
id: `plan-${Date.now()}`,
|
||||
name: data.name,
|
||||
template_id: data.template_id,
|
||||
mode: data.mode,
|
||||
clips: data.clips.map((c, i) => ({
|
||||
...c,
|
||||
id: `clip-${Date.now()}-${i}`,
|
||||
})),
|
||||
total_duration: data.clips.reduce((sum, c) => sum + c.duration, 0),
|
||||
status: "draft",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
mockPlans = [plan, ...mockPlans];
|
||||
return plan;
|
||||
}
|
||||
|
||||
/** 更新剪辑计划 */
|
||||
export async function updateEditPlan(
|
||||
id: string,
|
||||
data: UpdateEditPlanRequest
|
||||
): Promise<EditPlan> {
|
||||
// TODO: 替换为 apiClient.put(`/edit-plans/${id}`, data)
|
||||
await delay();
|
||||
const idx = mockPlans.findIndex((p) => p.id === id);
|
||||
if (idx === -1) throw new Error(`剪辑计划 ${id} 不存在`);
|
||||
mockPlans[idx] = {
|
||||
...mockPlans[idx],
|
||||
...data,
|
||||
total_duration:
|
||||
data.clips?.reduce((sum, c) => sum + c.duration, 0) ??
|
||||
mockPlans[idx].total_duration,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
return { ...mockPlans[idx] };
|
||||
}
|
||||
|
||||
/** 删除剪辑计划 */
|
||||
export async function deleteEditPlan(id: string): Promise<void> {
|
||||
// TODO: 替换为 apiClient.delete(`/edit-plans/${id}`)
|
||||
await delay();
|
||||
mockPlans = mockPlans.filter((p) => p.id !== id);
|
||||
}
|
||||
|
||||
/** 获取素材库列表 */
|
||||
export async function getMediaAssets(): Promise<MediaAsset[]> {
|
||||
// TODO: 替换为 apiClient.get('/media-assets')
|
||||
await delay();
|
||||
return [...MOCK_ASSETS];
|
||||
}
|
||||
|
||||
/** 获取单个素材 */
|
||||
export async function getMediaAsset(id: string): Promise<MediaAsset> {
|
||||
// TODO: 替换为 apiClient.get(`/media-assets/${id}`)
|
||||
await delay();
|
||||
const asset = MOCK_ASSETS.find((a) => a.id === id);
|
||||
if (!asset) throw new Error(`素材 ${id} 不存在`);
|
||||
return { ...asset };
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 常量
|
||||
* ============================================================ */
|
||||
|
||||
/** 转场效果选项 */
|
||||
export const TRANSITION_OPTIONS: {
|
||||
value: TransitionEffect["type"];
|
||||
label: string;
|
||||
}[] = [
|
||||
{ value: "none", label: "无转场" },
|
||||
{ value: "fade", label: "淡入淡出" },
|
||||
{ value: "dissolve", label: "溶解" },
|
||||
{ value: "wipe", label: "擦除" },
|
||||
{ value: "zoom", label: "缩放" },
|
||||
{ value: "slide", label: "滑动" },
|
||||
];
|
||||
|
||||
/** 素材类型标签 */
|
||||
export const MATERIAL_TYPE_LABELS: Record<string, string> = {
|
||||
video: "视频",
|
||||
image: "图片",
|
||||
audio: "音频",
|
||||
voiceover: "配音",
|
||||
};
|
||||
|
||||
/** 素材类型图标 */
|
||||
export const MATERIAL_TYPE_ICONS: Record<string, string> = {
|
||||
video: "🎬",
|
||||
image: "🖼️",
|
||||
audio: "🎵",
|
||||
voiceover: "🎙️",
|
||||
};
|
||||
|
||||
/** 计划状态标签 */
|
||||
export const PLAN_STATUS_LABELS: Record<EditPlan["status"], string> = {
|
||||
draft: "草稿",
|
||||
ready: "就绪",
|
||||
generating: "生成中",
|
||||
completed: "已完成",
|
||||
failed: "失败",
|
||||
};
|
||||
Reference in New Issue
Block a user