feat: 剪辑计划片段CRUD前端全链路对接 (#403)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI Build & Deploy Pipeline / Build Staging API Image (push) Successful in 4m7s
CI Build & Deploy Pipeline / Build Staging Web Image (push) Successful in 1m40s
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Successful in 10m24s
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m4s
CI Build & Deploy Pipeline / Staging E2E Tests (push) Waiting to run
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Waiting to run
CI Build & Deploy Pipeline / Build Production API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (push) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been skipped

feat: 剪辑计划片段CRUD前端全链路对接 (#403)
This commit was merged in pull request #403.
This commit is contained in:
2026-07-17 09:59:13 +08:00
parent 47e7018f2a
commit abe854be76
10 changed files with 2073 additions and 67 deletions
+214 -18
View File
@@ -276,24 +276,6 @@ export interface CoverResult {
* 前端 UI 类型(EditingPlanner 组件依赖,保留兼容)
* ============================================================ */
/** 剪辑计划中的片段(UI 层类型) */
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;
}
/** 转场效果(14 种预设) */
export interface TransitionEffect {
type:
@@ -458,6 +440,220 @@ export async function cancelGeneration(planId: string): Promise<void> {
await apiClient.post(`/edit-plans/${planId}/cancel`);
}
/* ============================================================
* 片段 CRUD(后端 EditPlanClip 独立表)
* ============================================================ */
/** 片段状态 */
export type EditPlanClipStatus = "pending" | "processing" | "ready" | "failed";
/** 剪辑片段(后端响应) */
export interface EditPlanClip {
id: string;
plan_id: string;
clip_type: string; // main / intro / outro / overlay / background / b_roll 等
order: number;
asset_id: string;
text_content: string;
start_time: number;
duration: number;
transition_effect: string;
transition_duration: number;
playback_speed: number;
status: EditPlanClipStatus;
config: Record<string, unknown>;
created_at?: string;
updated_at?: string;
}
/** 创建片段请求 */
export interface CreateEditPlanClipRequest {
clip_type: string;
order: number;
asset_id?: string;
text_content?: string;
start_time?: number;
duration?: number;
transition_effect?: string;
transition_duration?: number;
playback_speed?: number;
config?: Record<string, unknown>;
}
/** 更新片段请求 */
export interface UpdateEditPlanClipRequest {
clip_type?: string;
order?: number;
asset_id?: string;
text_content?: string;
start_time?: number;
duration?: number;
transition_effect?: string;
transition_duration?: number;
playback_speed?: number;
config?: Record<string, unknown>;
}
/** 片段列表响应 */
export interface EditPlanClipListResponse {
items: EditPlanClip[];
total: number;
}
/** 片段列表查询参数 */
export interface EditPlanClipListParams {
status?: string;
skip?: number;
limit?: number;
}
/** 获取片段列表 */
export async function getEditPlanClips(
planId: string,
params?: EditPlanClipListParams,
): Promise<EditPlanClipListResponse> {
const response = await apiClient.get<EditPlanClipListResponse>(
`/edit-plans/${planId}/clips`,
{ params },
);
return response.data;
}
/** 获取单个片段详情 */
export async function getEditPlanClip(
planId: string,
clipId: string,
): Promise<EditPlanClip> {
const response = await apiClient.get<EditPlanClip>(
`/edit-plans/${planId}/clips/${clipId}`,
);
return response.data;
}
/** 创建片段 */
export async function createEditPlanClip(
planId: string,
data: CreateEditPlanClipRequest,
): Promise<EditPlanClip> {
const response = await apiClient.post<EditPlanClip>(
`/edit-plans/${planId}/clips`,
data,
);
return response.data;
}
/** 更新片段 */
export async function updateEditPlanClip(
planId: string,
clipId: string,
data: UpdateEditPlanClipRequest,
): Promise<EditPlanClip> {
const response = await apiClient.put<EditPlanClip>(
`/edit-plans/${planId}/clips/${clipId}`,
data,
);
return response.data;
}
/** 删除片段 */
export async function deleteEditPlanClip(
planId: string,
clipId: string,
): Promise<void> {
await apiClient.delete(`/edit-plans/${planId}/clips/${clipId}`);
}
/* ============================================================
* 片段批量操作
* ============================================================ */
/** 重排序条目 */
export interface ClipReorderItem {
clip_id: string;
new_order: number;
}
/** 重排序响应 */
export interface ClipReorderResponse {
success: boolean;
updated_count: number;
message: string;
}
/** 批量删除响应 */
export interface ClipBatchDeleteResponse {
success: boolean;
deleted_count: number;
message: string;
}
/** 从素材批量创建响应 */
export interface ClipsFromAssetsResponse {
success: boolean;
created_count: number;
message: string;
clip_ids: string[];
}
/** 片段重排序(拖拽排序后一次性提交) */
export async function reorderEditPlanClips(
planId: string,
items: ClipReorderItem[],
): Promise<ClipReorderResponse> {
const response = await apiClient.post<ClipReorderResponse>(
`/edit-plans/${planId}/clips/reorder`,
{ items },
);
return response.data;
}
/** 批量删除片段 */
export async function batchDeleteEditPlanClips(
planId: string,
clipIds: string[],
): Promise<ClipBatchDeleteResponse> {
const response = await apiClient.post<ClipBatchDeleteResponse>(
`/edit-plans/${planId}/clips/batch-delete`,
{ clip_ids: clipIds },
);
return response.data;
}
/** 从素材批量创建片段(追加到时间线末尾) */
export async function createClipsFromAssets(
planId: string,
assetIds: string[],
clipType = "main",
): Promise<ClipsFromAssetsResponse> {
const response = await apiClient.post<ClipsFromAssetsResponse>(
`/edit-plans/${planId}/clips/from-assets`,
{ asset_ids: assetIds, clip_type: clipType },
);
return response.data;
}
/* ============================================================
* 复制计划
* ============================================================ */
/** 复制计划请求 */
export interface CopyEditPlanRequest {
name?: string;
project_id?: string;
}
/** 复制剪辑计划(含所有片段配置) */
export async function copyEditPlan(
planId: string,
data?: CopyEditPlanRequest,
): Promise<EditPlan> {
const response = await apiClient.post<EditPlan>(
`/edit-plans/${planId}/copy`,
data || {},
);
return response.data;
}
/**
* 获取素材库列表 — 调用 GET /api/v1/assets?library_id=xxx
* 将后端 AssetResponse 映射为前端 MediaAsset 类型