Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d05aa0bcc |
@@ -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<EditPlanItem[]> => {
|
|
||||||
const response = await apiClient.get('/edit-plans');
|
|
||||||
return response.data.items || [];
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 获取单个编辑计划 */
|
|
||||||
export const getEditPlan = async (planId: string): Promise<EditPlanItem> => {
|
|
||||||
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<EditPlanItem> => {
|
|
||||||
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<EditPlanItem> => {
|
|
||||||
const response = await apiClient.post('/edit-plans/auto-generate', params);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 更新编辑计划 */
|
|
||||||
export const updateEditPlan = async (
|
|
||||||
planId: string,
|
|
||||||
data: Partial<EditPlanItem>
|
|
||||||
): Promise<EditPlanItem> => {
|
|
||||||
const response = await apiClient.patch(`/edit-plans/${planId}`, data);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 删除编辑计划 */
|
|
||||||
export const deleteEditPlan = async (planId: string): Promise<void> => {
|
|
||||||
await apiClient.delete(`/edit-plans/${planId}`);
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* 剪辑计划编辑器 API
|
* 剪辑计划编辑器 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 {
|
export interface TemplateSegment {
|
||||||
id: string;
|
id?: string;
|
||||||
segment_order: number;
|
segment_order: number;
|
||||||
duration_min: number;
|
duration_min: number;
|
||||||
duration_max: number;
|
duration_max: number;
|
||||||
material_type: string | null; // 仅 口播+混剪 模式:人物/场景
|
material_type: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 剪辑模板 */
|
/** 剪辑模板 */
|
||||||
@@ -72,6 +72,7 @@ export interface EditingTemplate {
|
|||||||
bgm_config: BgmConfig;
|
bgm_config: BgmConfig;
|
||||||
estimated_duration: number;
|
estimated_duration: number;
|
||||||
segments: TemplateSegment[];
|
segments: TemplateSegment[];
|
||||||
|
is_active?: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -80,6 +81,7 @@ export interface EditingTemplate {
|
|||||||
export interface TemplateCategory {
|
export interface TemplateCategory {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
created_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 创建/更新模板请求体 */
|
/** 创建/更新模板请求体 */
|
||||||
@@ -100,124 +102,46 @@ export interface GenerateFromTemplatePayload {
|
|||||||
voiceover_duration: number;
|
voiceover_duration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 使用模板生成响应 */
|
/** 验证/生成响应 */
|
||||||
export interface GenerateFromTemplateResponse {
|
export interface ValidateWarning {
|
||||||
task_id: string;
|
code: string;
|
||||||
warning?: string;
|
message: string;
|
||||||
|
details?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ──────────── 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: '生活' },
|
export interface ListCategoriesResponse {
|
||||||
{ id: 'cat-2', name: '美食' },
|
items: TemplateCategory[];
|
||||||
{ id: 'cat-3', name: '旅行' },
|
}
|
||||||
{ id: 'cat-4', name: '知识' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const MOCK_TEMPLATES: EditingTemplate[] = [
|
// ============ API 函数 ============
|
||||||
{
|
|
||||||
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));
|
|
||||||
|
|
||||||
/** 获取模板列表 */
|
/** 获取模板列表 */
|
||||||
export const getEditingTemplates = async (params?: {
|
export const getEditingTemplates = async (params?: {
|
||||||
category?: string;
|
category?: string;
|
||||||
tag?: string;
|
tag?: string;
|
||||||
|
skip?: number;
|
||||||
|
limit?: number;
|
||||||
}): Promise<EditingTemplate[]> => {
|
}): Promise<EditingTemplate[]> => {
|
||||||
await delay();
|
const response = await apiClient.get<ListTemplatesResponse>('/templates', {
|
||||||
let list = [...MOCK_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?.category) list = list.filter((t) => t.category === params.category);
|
||||||
if (params?.tag) list = list.filter((t) => t.tags.includes(params.tag!));
|
if (params?.tag) list = list.filter((t) => t.tags.includes(params.tag!));
|
||||||
return list;
|
return list;
|
||||||
@@ -225,38 +149,16 @@ export const getEditingTemplates = async (params?: {
|
|||||||
|
|
||||||
/** 获取模板详情 */
|
/** 获取模板详情 */
|
||||||
export const getEditingTemplate = async (id: string): Promise<EditingTemplate> => {
|
export const getEditingTemplate = async (id: string): Promise<EditingTemplate> => {
|
||||||
await delay();
|
const response = await apiClient.get<EditingTemplate>(`/templates/${id}`);
|
||||||
const tpl = MOCK_TEMPLATES.find((t) => t.id === id);
|
return response.data;
|
||||||
if (!tpl) throw new Error('模板不存在');
|
|
||||||
return { ...tpl };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 创建模板 */
|
/** 创建模板 */
|
||||||
export const createEditingTemplate = async (
|
export const createEditingTemplate = async (
|
||||||
data: SaveTemplatePayload,
|
data: SaveTemplatePayload,
|
||||||
): Promise<EditingTemplate> => {
|
): Promise<EditingTemplate> => {
|
||||||
await delay(300);
|
const response = await apiClient.post<EditingTemplate>('/templates', data);
|
||||||
const now = new Date().toISOString();
|
return response.data;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 更新模板 */
|
/** 更新模板 */
|
||||||
@@ -264,51 +166,29 @@ export const updateEditingTemplate = async (
|
|||||||
id: string,
|
id: string,
|
||||||
data: SaveTemplatePayload,
|
data: SaveTemplatePayload,
|
||||||
): Promise<EditingTemplate> => {
|
): Promise<EditingTemplate> => {
|
||||||
await delay(300);
|
const response = await apiClient.patch<EditingTemplate>(`/templates/${id}`, data);
|
||||||
const idx = MOCK_TEMPLATES.findIndex((t) => t.id === id);
|
return response.data;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 删除模板 */
|
/** 删除模板 */
|
||||||
export const deleteEditingTemplate = async (id: string): Promise<void> => {
|
export const deleteEditingTemplate = async (id: string): Promise<void> => {
|
||||||
await delay();
|
await apiClient.delete(`/templates/${id}`);
|
||||||
const idx = MOCK_TEMPLATES.findIndex((t) => t.id === id);
|
|
||||||
if (idx !== -1) MOCK_TEMPLATES.splice(idx, 1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 获取模板分类列表 */
|
/** 获取模板分类列表 */
|
||||||
export const getTemplateCategories = async (): Promise<TemplateCategory[]> => {
|
export const getTemplateCategories = async (): Promise<TemplateCategory[]> => {
|
||||||
await delay();
|
const response = await apiClient.get<ListCategoriesResponse>('/templates/categories/list');
|
||||||
return [...MOCK_CATEGORIES];
|
return response.data.items;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 使用模板生成视频 */
|
/** 使用模板生成视频(调用 validate 端点) */
|
||||||
export const generateFromTemplate = async (
|
export const generateFromTemplate = async (
|
||||||
_templateId: string,
|
templateId: string,
|
||||||
_data: GenerateFromTemplatePayload,
|
data: GenerateFromTemplatePayload,
|
||||||
): Promise<GenerateFromTemplateResponse> => {
|
): Promise<GenerateFromTemplateResponse> => {
|
||||||
await delay(500);
|
const response = await apiClient.post<GenerateFromTemplateResponse>(
|
||||||
return {
|
`/templates/${templateId}/validate`,
|
||||||
task_id: nextId(),
|
data,
|
||||||
warning: undefined,
|
);
|
||||||
};
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user