fix: resolve all flake8 errors and apply Prettier formatting (#131)
Deploy / Deploy Staging (push) Failing after 210h26m26s
CI/CD Pipeline / Frontend Lint (push) Failing after 210h27m0s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 210h27m7s
Deploy / Production Browser E2E (push) Failing after 1869h1m52s
Deploy / Build Production Runtime Images (push) Failing after 1869h1m56s
Deploy / Deploy Production (push) Failing after 1869h33m15s

This commit was merged in pull request #131.
This commit is contained in:
2026-06-30 18:26:53 +08:00
parent 1217d8cef0
commit 560856cf22
76 changed files with 2161 additions and 1540 deletions
+22 -22
View File
@@ -2,8 +2,8 @@
* 素材相关 API
* Phase 1 重构:去掉 project_id,素材直接归属用户
*/
import apiClient from './client';
import { getOrCreateDefaultProject } from './projects';
import apiClient from "./client";
import { getOrCreateDefaultProject } from "./projects";
/** 素材条目 */
export interface AssetItem {
@@ -24,7 +24,7 @@ export interface AssetItem {
export interface AssetLibraryItem {
id: string;
name: string;
kind: 'video' | 'voice' | 'image';
kind: "video" | "voice" | "image";
asset_count?: number;
total_size?: number;
created_at?: string;
@@ -35,7 +35,7 @@ export interface IngestJob {
id: string;
library_id: string;
storage_key: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
status: "pending" | "processing" | "completed" | "failed";
error_message: string;
result_asset_id: string;
}
@@ -44,7 +44,7 @@ export interface IngestJob {
export interface ClassificationJob {
id: string;
asset_id: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
status: "pending" | "processing" | "completed" | "failed";
classification: string;
confidence: number;
error_message: string;
@@ -72,7 +72,7 @@ export interface AssetDiagnosis {
}>;
gaps: Array<{
key: string;
severity: 'critical' | 'warning' | 'info';
severity: "critical" | "warning" | "info";
message: string;
recommendation: string;
}>;
@@ -82,7 +82,7 @@ export interface AssetDiagnosis {
/** 获取当前用户的素材诊断信息 */
export const getAssetDiagnosis = async (): Promise<AssetDiagnosis> => {
const response = await apiClient.get('/asset-diagnosis');
const response = await apiClient.get("/asset-diagnosis");
return response.data;
};
@@ -90,18 +90,18 @@ export const getAssetDiagnosis = async (): Promise<AssetDiagnosis> => {
/** 获取当前用户的所有素材库 */
export const getAssetLibraries = async (): Promise<AssetLibraryItem[]> => {
const response = await apiClient.get('/asset-libraries');
const response = await apiClient.get("/asset-libraries");
return response.data.items || [];
};
/** 创建素材库(自动获取或创建默认项目以提供 project_id) */
export const createAssetLibrary = async (data: {
name: string;
kind: 'video' | 'voice' | 'image';
kind: "video" | "voice" | "image";
}): Promise<AssetLibraryItem> => {
// 后端要求 project_id,前端自动管理默认项目
const project = await getOrCreateDefaultProject();
const response = await apiClient.post('/asset-libraries', {
const response = await apiClient.post("/asset-libraries", {
project_id: project.id,
...data,
});
@@ -117,7 +117,7 @@ export const deleteAssetLibrary = async (libraryId: string): Promise<void> => {
/** 获取素材库下的所有素材 */
export const getAssets = async (libraryId: string): Promise<AssetItem[]> => {
const response = await apiClient.get('/assets', {
const response = await apiClient.get("/assets", {
params: { library_id: libraryId },
});
return response.data.items || [];
@@ -126,7 +126,7 @@ export const getAssets = async (libraryId: string): Promise<AssetItem[]> => {
/** 更新素材审核状态 */
export const updateAssetReviewStatus = async (
assetId: string,
reviewStatus: 'pending_review' | 'approved' | 'rejected'
reviewStatus: "pending_review" | "approved" | "rejected",
): Promise<AssetItem> => {
const response = await apiClient.patch(`/assets/${assetId}/review`, {
review_status: reviewStatus,
@@ -143,10 +143,10 @@ export const deleteAsset = async (assetId: string): Promise<void> => {
/** 表单上传素材(小文件) */
export const uploadAsset = async (
formData: FormData
formData: FormData,
): Promise<{ storage_key: string; ingest_job_id: string; url: string }> => {
const response = await apiClient.post('/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
const response = await apiClient.post("/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
timeout: 30 * 60 * 1000,
});
return response.data;
@@ -167,7 +167,7 @@ export const prepareDirectUpload = async (data: {
fields: Record<string, string>;
max_size_bytes: number;
}> => {
const response = await apiClient.post('/upload/direct/prepare', data);
const response = await apiClient.post("/upload/direct/prepare", data);
return response.data;
};
@@ -177,7 +177,7 @@ export const completeDirectUpload = async (data: {
library_id: string;
storage_key: string;
}): Promise<{ storage_key: string; ingest_job_id: string }> => {
const response = await apiClient.post('/upload/direct/complete', data);
const response = await apiClient.post("/upload/direct/complete", data);
return response.data;
};
@@ -193,15 +193,15 @@ export const uploadAssetDirect = async (data: {
project_id: project.id,
library_id: data.library_id,
filename: data.file.name,
content_type: data.file.type || 'application/octet-stream',
content_type: data.file.type || "application/octet-stream",
file_size: data.file.size,
});
const directForm = new FormData();
Object.entries(prepared.fields).forEach(([key, value]) =>
directForm.append(key, value)
directForm.append(key, value),
);
directForm.append('file', data.file);
directForm.append("file", data.file);
const uploadResponse = await fetch(prepared.upload_url, {
method: prepared.method,
@@ -230,13 +230,13 @@ export const getIngestJob = async (jobId: string): Promise<IngestJob> => {
export const submitClassificationJob = async (data: {
asset_id: string;
}): Promise<ClassificationJob> => {
const response = await apiClient.post('/classification-jobs', data);
const response = await apiClient.post("/classification-jobs", data);
return response.data;
};
/** 查询分类任务状态 */
export const getClassificationJob = async (
jobId: string
jobId: string,
): Promise<ClassificationJob> => {
const response = await apiClient.get(`/classification-jobs/${jobId}`);
return response.data;
+27 -27
View File
@@ -1,47 +1,47 @@
import { describe, expect, it } from 'vitest';
import { normalizeUser } from './auth';
import { describe, expect, it } from "vitest";
import { normalizeUser } from "./auth";
describe('normalizeUser', () => {
it('normalizes canonical API current-user fields', () => {
describe("normalizeUser", () => {
it("normalizes canonical API current-user fields", () => {
expect(
normalizeUser({
user_id: 'user-1',
email: 'user@example.com',
username: 'user',
display_name: 'User',
user_id: "user-1",
email: "user@example.com",
username: "user",
display_name: "User",
email_verified: true,
})
}),
).toEqual({
id: 'user-1',
user_id: 'user-1',
email: 'user@example.com',
username: 'user',
display_name: 'User',
id: "user-1",
user_id: "user-1",
email: "user@example.com",
username: "user",
display_name: "User",
is_email_verified: true,
email_verified: true,
created_at: undefined,
});
});
it('keeps compatibility with legacy UI-shaped user fields', () => {
it("keeps compatibility with legacy UI-shaped user fields", () => {
expect(
normalizeUser({
id: 'user-2',
email: 'legacy@example.com',
username: 'legacy',
display_name: 'Legacy',
id: "user-2",
email: "legacy@example.com",
username: "legacy",
display_name: "Legacy",
is_email_verified: false,
created_at: '2026-06-22T00:00:00Z',
})
created_at: "2026-06-22T00:00:00Z",
}),
).toEqual({
id: 'user-2',
user_id: 'user-2',
email: 'legacy@example.com',
username: 'legacy',
display_name: 'Legacy',
id: "user-2",
user_id: "user-2",
email: "legacy@example.com",
username: "legacy",
display_name: "Legacy",
is_email_verified: false,
email_verified: false,
created_at: '2026-06-22T00:00:00Z',
created_at: "2026-06-22T00:00:00Z",
});
});
});
+19 -13
View File
@@ -1,7 +1,7 @@
/**
* 认证相关 API
*/
import apiClient from './client';
import apiClient from "./client";
// 类型定义
export interface LoginRequest {
@@ -50,7 +50,7 @@ export interface UserResponse {
}
export const normalizeUser = (data: UserResponse): User => {
const userId = data.id ?? data.user_id ?? '';
const userId = data.id ?? data.user_id ?? "";
const emailVerified = data.is_email_verified ?? data.email_verified ?? false;
return {
@@ -67,39 +67,43 @@ export const normalizeUser = (data: UserResponse): User => {
// 登录
export const login = async (data: LoginRequest): Promise<LoginResponse> => {
const response = await apiClient.post('/auth/login', data);
const response = await apiClient.post("/auth/login", data);
return response.data;
};
// 注册
export const register = async (data: RegisterRequest): Promise<{ message: string }> => {
const response = await apiClient.post('/auth/register', data);
export const register = async (
data: RegisterRequest,
): Promise<{ message: string }> => {
const response = await apiClient.post("/auth/register", data);
return response.data;
};
// 登出
export const logout = async (): Promise<void> => {
await apiClient.post('/auth/logout');
await apiClient.post("/auth/logout");
};
// 获取当前用户
export const getCurrentUser = async (): Promise<User> => {
const response = await apiClient.get<UserResponse>('/auth/me');
const response = await apiClient.get<UserResponse>("/auth/me");
return normalizeUser(response.data);
};
// 请求密码重置
export const requestPasswordReset = async (email: string): Promise<{ message: string }> => {
const response = await apiClient.post('/auth/forgot-password', { email });
export const requestPasswordReset = async (
email: string,
): Promise<{ message: string }> => {
const response = await apiClient.post("/auth/forgot-password", { email });
return response.data;
};
// 重置密码
export const resetPassword = async (
token: string,
newPassword: string
newPassword: string,
): Promise<{ message: string }> => {
const response = await apiClient.post('/auth/reset-password', {
const response = await apiClient.post("/auth/reset-password", {
token,
new_password: newPassword,
});
@@ -107,7 +111,9 @@ export const resetPassword = async (
};
// 验证邮箱
export const verifyEmail = async (token: string): Promise<{ message: string }> => {
const response = await apiClient.post('/auth/verify-email', { token });
export const verifyEmail = async (
token: string,
): Promise<{ message: string }> => {
const response = await apiClient.post("/auth/verify-email", { token });
return response.data;
};
+18 -16
View File
@@ -2,23 +2,23 @@
* API 客户端配置
* 封装 Axios 实例,配置拦截器和 Token 管理
*/
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { message } from 'antd';
import { useAuthStore } from '@/store/authStore';
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios";
import { message } from "antd";
import { useAuthStore } from "@/store/authStore";
// 创建 Axios 实例
const apiClient = axios.create({
baseURL: '/api/v1',
baseURL: "/api/v1",
timeout: 10000,
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
});
// 请求拦截器:添加 Token
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('access_token');
const token = localStorage.getItem("access_token");
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
@@ -26,13 +26,15 @@ apiClient.interceptors.request.use(
},
(error: AxiosError) => {
return Promise.reject(error);
}
},
);
// 响应拦截器:统一错误提示 + 处理未授权状态
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError<{ detail?: string; message?: string; msg?: string }>) => {
async (
error: AxiosError<{ detail?: string; message?: string; msg?: string }>,
) => {
// 401 → 清除登录态
if (error.response?.status === 401) {
useAuthStore.getState().clearAuth();
@@ -43,11 +45,11 @@ apiClient.interceptors.response.use(
const serverMsg = data?.detail || data?.message || data?.msg;
let handled = false;
if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
message.error('请求超时,请检查网络后重试');
if (error.code === "ECONNABORTED" || error.message?.includes("timeout")) {
message.error("请求超时,请检查网络后重试");
handled = true;
} else if (!error.response) {
message.error('网络连接异常,请检查网络设置');
message.error("网络连接异常,请检查网络设置");
handled = true;
} else if (serverMsg) {
message.error(serverMsg);
@@ -55,16 +57,16 @@ apiClient.interceptors.response.use(
} else {
const status = error.response?.status;
if (status === 413) {
message.error('文件过大,请缩小后重试');
message.error("文件过大,请缩小后重试");
handled = true;
} else if (status === 415) {
message.error('不支持的文件格式');
message.error("不支持的文件格式");
handled = true;
} else if (status === 503) {
message.error('服务暂不可用,请稍后再试');
message.error("服务暂不可用,请稍后再试");
handled = true;
} else if (status && status >= 500) {
message.error('服务器繁忙,请稍后再试');
message.error("服务器繁忙,请稍后再试");
handled = true;
}
// 其他 4xx 且无具体信息时不弹通用提示,由各组件自行处理
@@ -77,7 +79,7 @@ apiClient.interceptors.response.use(
}
return Promise.reject(error);
}
},
);
export default apiClient;
+7 -8
View File
@@ -2,7 +2,7 @@
* 仪表盘 API
* Phase 1 新增:用户仪表盘概览
*/
import apiClient from './client';
import apiClient from "./client";
/** 仪表盘概览数据 */
export interface DashboardOverview {
@@ -29,15 +29,14 @@ export interface DashboardOverview {
}>;
/** 订阅信息 */
subscription: {
plan: 'free' | 'pro' | 'enterprise';
status: 'active' | 'inactive' | 'expired';
plan: "free" | "pro" | "enterprise";
status: "active" | "inactive" | "expired";
expires_at?: string;
};
}
/** 获取仪表盘概览数据 */
export const getDashboardOverview =
async (): Promise<DashboardOverview> => {
const response = await apiClient.get('/dashboard/overview');
return response.data;
};
export const getDashboardOverview = async (): Promise<DashboardOverview> => {
const response = await apiClient.get("/dashboard/overview");
return response.data;
};
+51 -48
View File
@@ -2,10 +2,11 @@
* 查重 API 模块
* 提供视频查重相关接口(当前使用 mock 数据,后端就绪后切换)
*/
import apiClient from './client';
import apiClient from "./client";
/** 查重记录状态 */
export type DuplicationStatus = 'pending' | 'processing' | 'completed' | 'failed';
export type DuplicationStatus =
"pending" | "processing" | "completed" | "failed";
/** 查重记录 */
export interface DuplicationRecord {
@@ -68,55 +69,55 @@ export interface DuplicationUploadResponse {
/** mock 查重记录列表 */
const MOCK_RECORDS: DuplicationRecord[] = [
{
id: 'dup-001',
filename: '日常vlog_01.mp4',
id: "dup-001",
filename: "日常vlog_01.mp4",
file_size: 125_000_000,
duration_seconds: 180,
status: 'completed',
status: "completed",
duplicate_rate: 23.5,
duplicate_count: 3,
created_at: '2026-06-27T10:00:00Z',
updated_at: '2026-06-27T10:05:00Z',
created_at: "2026-06-27T10:00:00Z",
updated_at: "2026-06-27T10:05:00Z",
},
{
id: 'dup-002',
filename: '美食分享_片段.mp4',
id: "dup-002",
filename: "美食分享_片段.mp4",
file_size: 45_000_000,
duration_seconds: 60,
status: 'completed',
status: "completed",
duplicate_rate: 5.2,
duplicate_count: 1,
created_at: '2026-06-27T11:30:00Z',
updated_at: '2026-06-27T11:32:00Z',
created_at: "2026-06-27T11:30:00Z",
updated_at: "2026-06-27T11:32:00Z",
},
{
id: 'dup-003',
filename: '旅行记录_巴黎.mp4',
id: "dup-003",
filename: "旅行记录_巴黎.mp4",
file_size: 320_000_000,
duration_seconds: 420,
status: 'processing',
created_at: '2026-06-28T09:00:00Z',
updated_at: '2026-06-28T09:00:00Z',
status: "processing",
created_at: "2026-06-28T09:00:00Z",
updated_at: "2026-06-28T09:00:00Z",
},
{
id: 'dup-004',
filename: '产品展示_新版.mp4',
id: "dup-004",
filename: "产品展示_新版.mp4",
file_size: 88_000_000,
duration_seconds: 90,
status: 'completed',
status: "completed",
duplicate_rate: 67.8,
duplicate_count: 8,
created_at: '2026-06-26T15:00:00Z',
updated_at: '2026-06-26T15:10:00Z',
created_at: "2026-06-26T15:00:00Z",
updated_at: "2026-06-26T15:10:00Z",
},
{
id: 'dup-005',
filename: '教程_剪辑技巧.mp4',
id: "dup-005",
filename: "教程_剪辑技巧.mp4",
file_size: 200_000_000,
duration_seconds: 300,
status: 'failed',
created_at: '2026-06-26T14:00:00Z',
updated_at: '2026-06-26T14:01:00Z',
status: "failed",
created_at: "2026-06-26T14:00:00Z",
updated_at: "2026-06-26T14:01:00Z",
},
];
@@ -125,31 +126,31 @@ const MOCK_DETAIL: DuplicationDetail = {
...MOCK_RECORDS[0],
segments: [
{
id: 'seg-001',
id: "seg-001",
source_start: 10,
source_end: 25,
matched_video_id: 'asset-101',
matched_video_name: '日常vlog_素材库.mp4',
matched_video_id: "asset-101",
matched_video_name: "日常vlog_素材库.mp4",
matched_start: 45,
matched_end: 60,
similarity: 92.3,
},
{
id: 'seg-002',
id: "seg-002",
source_start: 60,
source_end: 78,
matched_video_id: 'asset-205',
matched_video_name: '城市风光_合集.mp4',
matched_video_id: "asset-205",
matched_video_name: "城市风光_合集.mp4",
matched_start: 120,
matched_end: 138,
similarity: 85.7,
},
{
id: 'seg-003',
id: "seg-003",
source_start: 150,
source_end: 165,
matched_video_id: 'asset-310',
matched_video_name: '背景音乐_配套画面.mp4',
matched_video_id: "asset-310",
matched_video_name: "背景音乐_配套画面.mp4",
matched_start: 30,
matched_end: 45,
similarity: 78.1,
@@ -164,21 +165,21 @@ const USE_MOCK = true;
/** 上传视频进行查重 */
export const uploadForDuplication = async (
file: File
file: File,
): Promise<DuplicationUploadResponse> => {
if (USE_MOCK) {
// 模拟上传延迟
await new Promise((resolve) => setTimeout(resolve, 1500));
return {
id: `dup-${Date.now()}`,
status: 'processing',
status: "processing",
message: `文件 "${file.name}" 已上传,正在查重中...`,
};
}
const formData = new FormData();
formData.append('file', file);
const response = await apiClient.post('/duplication/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
formData.append("file", file);
const response = await apiClient.post("/duplication/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
return response.data;
};
@@ -189,13 +190,13 @@ export const getDuplicationRecords = async (): Promise<DuplicationRecord[]> => {
await new Promise((resolve) => setTimeout(resolve, 300));
return MOCK_RECORDS;
}
const response = await apiClient.get('/duplication/records');
const response = await apiClient.get("/duplication/records");
return response.data;
};
/** 获取查重详情 */
export const getDuplicationDetail = async (
recordId: string
recordId: string,
): Promise<DuplicationDetail> => {
if (USE_MOCK) {
await new Promise((resolve) => setTimeout(resolve, 300));
@@ -208,7 +209,7 @@ export const getDuplicationDetail = async (
/** 删除查重记录 */
export const deleteDuplicationRecord = async (
recordId: string
recordId: string,
): Promise<void> => {
if (USE_MOCK) {
await new Promise((resolve) => setTimeout(resolve, 200));
@@ -219,16 +220,18 @@ export const deleteDuplicationRecord = async (
/** 重新查重 */
export const retryDuplication = async (
recordId: string
recordId: string,
): Promise<DuplicationUploadResponse> => {
if (USE_MOCK) {
await new Promise((resolve) => setTimeout(resolve, 500));
return {
id: recordId,
status: 'processing',
message: '已重新提交查重',
status: "processing",
message: "已重新提交查重",
};
}
const response = await apiClient.post(`/duplication/records/${recordId}/retry`);
const response = await apiClient.post(
`/duplication/records/${recordId}/retry`,
);
return response.data;
};
+25 -17
View File
@@ -2,27 +2,27 @@
* 剪辑计划编辑器 API
* 对接后端 /api/v1/templates 路由
*/
import apiClient from './client';
import apiClient from "./client";
/* ──────────── 类型定义 ──────────── */
/** 模板模式(后端枚举值) */
export type TemplateMode = 'pip' | 'voice_over' | 'one_take' | 'voice_pip';
export type TemplateMode = "pip" | "voice_over" | "one_take" | "voice_pip";
/** 模式显示名称映射 */
export const MODE_LABELS: Record<TemplateMode, string> = {
pip: '画中画',
voice_over: '人物口播',
one_take: '一镜到底',
voice_pip: '口播+混剪',
pip: "画中画",
voice_over: "人物口播",
one_take: "一镜到底",
voice_pip: "口播+混剪",
};
/** 模式颜色映射 */
export const MODE_COLORS: Record<TemplateMode, string> = {
pip: 'blue',
voice_over: 'green',
one_take: 'orange',
voice_pip: 'purple',
pip: "blue",
voice_over: "green",
one_take: "orange",
voice_pip: "purple",
};
/** 标题配置 */
@@ -94,7 +94,7 @@ export interface SaveTemplatePayload {
subtitle_config: SubtitleConfig;
bgm_config: BgmConfig;
estimated_duration: number;
segments: Omit<TemplateSegment, 'id'>[];
segments: Omit<TemplateSegment, "id">[];
}
/** 使用模板生成请求体 */
@@ -135,20 +135,23 @@ export const getEditingTemplates = async (params?: {
skip?: number;
limit?: number;
}): Promise<EditingTemplate[]> => {
const response = await apiClient.get<ListTemplatesResponse>('/templates', {
const response = await apiClient.get<ListTemplatesResponse>("/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!));
return list;
};
/** 获取模板详情 */
export const getEditingTemplate = async (id: string): Promise<EditingTemplate> => {
export const getEditingTemplate = async (
id: string,
): Promise<EditingTemplate> => {
const response = await apiClient.get<EditingTemplate>(`/templates/${id}`);
return response.data;
};
@@ -157,7 +160,7 @@ export const getEditingTemplate = async (id: string): Promise<EditingTemplate> =
export const createEditingTemplate = async (
data: SaveTemplatePayload,
): Promise<EditingTemplate> => {
const response = await apiClient.post<EditingTemplate>('/templates', data);
const response = await apiClient.post<EditingTemplate>("/templates", data);
return response.data;
};
@@ -166,7 +169,10 @@ export const updateEditingTemplate = async (
id: string,
data: SaveTemplatePayload,
): Promise<EditingTemplate> => {
const response = await apiClient.patch<EditingTemplate>(`/templates/${id}`, data);
const response = await apiClient.patch<EditingTemplate>(
`/templates/${id}`,
data,
);
return response.data;
};
@@ -177,7 +183,9 @@ export const deleteEditingTemplate = async (id: string): Promise<void> => {
/** 获取模板分类列表 */
export const getTemplateCategories = async (): Promise<TemplateCategory[]> => {
const response = await apiClient.get<ListCategoriesResponse>('/templates/categories/list');
const response = await apiClient.get<ListCategoriesResponse>(
"/templates/categories/list",
);
return response.data.items;
};
+6 -10
View File
@@ -2,7 +2,7 @@
* 成品相关 API
* Phase 1 重构:去掉 projectId,成品直接归属用户
*/
import apiClient from './client';
import apiClient from "./client";
/** 成品条目 */
export interface ProductItem {
@@ -13,7 +13,7 @@ export interface ProductItem {
duration_seconds?: number;
file_size?: number;
resolution?: string;
status: 'processing' | 'completed' | 'failed';
status: "processing" | "completed" | "failed";
/** 查重率(百分比) */
duplicate_rate?: number;
created_at?: string;
@@ -22,14 +22,12 @@ export interface ProductItem {
/** 获取当前用户的所有成品 */
export const getProducts = async (): Promise<ProductItem[]> => {
const response = await apiClient.get('/products');
const response = await apiClient.get("/products");
return response.data.items || response.data || [];
};
/** 获取单个成品详情 */
export const getProduct = async (
productId: string
): Promise<ProductItem> => {
export const getProduct = async (productId: string): Promise<ProductItem> => {
const response = await apiClient.get(`/products/${productId}`);
return response.data;
};
@@ -41,10 +39,8 @@ export const deleteProduct = async (productId: string): Promise<void> => {
/** 获取成品下载链接 */
export const getProductDownloadUrl = async (
productId: string
productId: string,
): Promise<{ url: string; expires_at: string }> => {
const response = await apiClient.get(
`/products/${productId}/download-url`
);
const response = await apiClient.get(`/products/${productId}/download-url`);
return response.data;
};
+7 -6
View File
@@ -2,7 +2,7 @@
* 项目相关 API
* 素材库需要 project_id,前端自动管理默认项目
*/
import apiClient from './client';
import apiClient from "./client";
export interface ProjectItem {
id: string;
@@ -30,7 +30,8 @@ const toProjectItem = (item: BackendProjectResponse): ProjectItem => ({
/** 获取当前用户的项目列表 */
export const getProjects = async (): Promise<ProjectItem[]> => {
const response = await apiClient.get<BackendListProjectsResponse>('/projects');
const response =
await apiClient.get<BackendListProjectsResponse>("/projects");
return (response.data.items || []).map(toProjectItem);
};
@@ -39,9 +40,9 @@ export const createProject = async (data: {
name: string;
description?: string;
}): Promise<ProjectItem> => {
const response = await apiClient.post<BackendProjectResponse>('/projects', {
const response = await apiClient.post<BackendProjectResponse>("/projects", {
name: data.name,
description: data.description || '',
description: data.description || "",
});
return toProjectItem(response.data);
};
@@ -54,7 +55,7 @@ export const getOrCreateDefaultProject = async (): Promise<ProjectItem> => {
}
// 没有项目时自动创建默认项目
return createProject({
name: '默认项目',
description: '系统自动创建的默认项目',
name: "默认项目",
description: "系统自动创建的默认项目",
});
};
+22 -13
View File
@@ -2,19 +2,19 @@
* 订阅 API 模块
* 对接后端订阅管理接口
*/
import apiClient from './client';
import apiClient from "./client";
/** 套餐类型 */
export type PlanType = 'free' | 'standard' | 'pro' | 'enterprise';
export type PlanType = "free" | "standard" | "pro" | "enterprise";
/** 订阅状态 */
export type SubscriptionStatus = 'active' | 'expired' | 'cancelled' | 'trial';
export type SubscriptionStatus = "active" | "expired" | "cancelled" | "trial";
/** 账单状态 */
export type BillingStatus = 'paid' | 'pending' | 'failed' | 'refunded';
export type BillingStatus = "paid" | "pending" | "failed" | "refunded";
/** 计费周期 */
export type BillingCycle = 'monthly' | 'yearly';
export type BillingCycle = "monthly" | "yearly";
/** 套餐信息 */
export interface Plan {
@@ -70,30 +70,39 @@ export interface ChangePlanResponse {
/** 获取当前订阅信息 */
export const getCurrentSubscription = async (): Promise<SubscriptionInfo> => {
const response = await apiClient.get('/subscription/current');
const response = await apiClient.get("/subscription/current");
return response.data;
};
/** 获取账单记录列表 */
export const getBillingRecords = async (): Promise<BillingRecord[]> => {
const response = await apiClient.get('/subscription/billing-records');
const response = await apiClient.get("/subscription/billing-records");
return response.data;
};
/** 升级/降级套餐 */
export const changePlan = async (request: ChangePlanRequest): Promise<ChangePlanResponse> => {
const response = await apiClient.post('/subscription/change-plan', request);
export const changePlan = async (
request: ChangePlanRequest,
): Promise<ChangePlanResponse> => {
const response = await apiClient.post("/subscription/change-plan", request);
return response.data;
};
/** 取消订阅 */
export const cancelSubscription = async (): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.post('/subscription/cancel');
export const cancelSubscription = async (): Promise<{
success: boolean;
message: string;
}> => {
const response = await apiClient.post("/subscription/cancel");
return response.data;
};
/** 切换自动续费 */
export const toggleAutoRenew = async (enabled: boolean): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.post('/subscription/toggle-auto-renew', { enabled });
export const toggleAutoRenew = async (
enabled: boolean,
): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.post("/subscription/toggle-auto-renew", {
enabled,
});
return response.data;
};
+4 -4
View File
@@ -5,14 +5,14 @@
* - GET /api/v1/tasks — 用户级任务列表(跨 project)
* - POST /api/v1/tasks/{task_id}/retry — 简化重试
*/
import apiClient from './client';
import apiClient from "./client";
/* ──────────── 类型定义 ──────────── */
/** 任务条目(对应用户级 UserTaskResponse) */
export interface TaskItem {
id: string;
task_type: 'ingest' | 'generation' | string;
task_type: "ingest" | "generation" | string;
project_id: string;
template_id: string;
status: string;
@@ -58,7 +58,7 @@ export const createGenerationTask = async (
params: CreateGenerationTaskRequest,
): Promise<CreateGenerationTaskResponse> => {
const { data } = await apiClient.post<CreateGenerationTaskResponse>(
'/generation/tasks',
"/generation/tasks",
params,
);
return data;
@@ -66,7 +66,7 @@ export const createGenerationTask = async (
/** 获取当前用户的所有任务(跨 project) */
export const getUserTasks = async (): Promise<TaskItem[]> => {
const { data } = await apiClient.get('/tasks');
const { data } = await apiClient.get("/tasks");
return data.items || [];
};
+5 -5
View File
@@ -2,7 +2,7 @@
* 模板相关 API
* Phase 1 新增:全局模板库
*/
import apiClient from './client';
import apiClient from "./client";
/** 模板条目 */
export interface TemplateItem {
@@ -21,13 +21,13 @@ export interface TemplateItem {
/** 获取全局模板列表 */
export const getTemplates = async (): Promise<TemplateItem[]> => {
const response = await apiClient.get('/templates');
const response = await apiClient.get("/templates");
return response.data.items || response.data || [];
};
/** 获取单个模板详情 */
export const getTemplate = async (
templateId: string
templateId: string,
): Promise<TemplateItem> => {
const response = await apiClient.get(`/templates/${templateId}`);
return response.data;
@@ -35,10 +35,10 @@ export const getTemplate = async (
/** 收藏 / 取消收藏模板 */
export const toggleFavoriteTemplate = async (
templateId: string
templateId: string,
): Promise<{ is_favorite: boolean }> => {
const response = await apiClient.post(
`/templates/${templateId}/toggle-favorite`
`/templates/${templateId}/toggle-favorite`,
);
return response.data;
};
+10 -5
View File
@@ -3,7 +3,7 @@
* Phase 1 新增:全局标题库
* 注意:后端 schema 使用 name + text 字段,前端 UI 用 content 展示
*/
import apiClient from './client';
import apiClient from "./client";
/** 标题条目(前端展示用) */
export interface TitleItem {
@@ -68,7 +68,9 @@ export interface CreateTitleRequest {
/** 获取当前用户的所有标题 */
export const getTitles = async (): Promise<TitleItem[]> => {
const response = await apiClient.get<{ items: BackendTitleResponse[] }>('/titles');
const response = await apiClient.get<{ items: BackendTitleResponse[] }>(
"/titles",
);
return (response.data.items || []).map(toTitleItem);
};
@@ -80,9 +82,12 @@ export const createTitle = async (
const payload: BackendCreateTitleRequest = {
name: data.content.slice(0, 255),
text: data.content.slice(0, 500),
category: data.category || 'default',
category: data.category || "default",
};
const response = await apiClient.post<BackendTitleResponse>('/titles', payload);
const response = await apiClient.post<BackendTitleResponse>(
"/titles",
payload,
);
return toTitleItem(response.data);
};
@@ -116,6 +121,6 @@ export const deleteTitle = async (titleId: string): Promise<void> => {
export const batchImportTitles = async (
titles: string[],
): Promise<{ imported_count: number }> => {
const response = await apiClient.post('/titles/batch-import', { titles });
const response = await apiClient.post("/titles/batch-import", { titles });
return response.data;
};
+6 -6
View File
@@ -2,7 +2,7 @@
* 配音相关 API
* Phase 1 新增:全局配音库
*/
import apiClient from './client';
import apiClient from "./client";
/** 配音条目 */
export interface VoiceItem {
@@ -28,22 +28,22 @@ export interface CreateVoiceRequest {
/** 获取当前用户的所有配音 */
export const getVoices = async (): Promise<VoiceItem[]> => {
const response = await apiClient.get('/voices');
const response = await apiClient.get("/voices");
return response.data.items || response.data || [];
};
/** 创建配音 */
export const createVoice = async (
data: CreateVoiceRequest
data: CreateVoiceRequest,
): Promise<VoiceItem> => {
const response = await apiClient.post('/voices', data);
const response = await apiClient.post("/voices", data);
return response.data;
};
/** 更新配音 */
export const updateVoice = async (
voiceId: string,
data: Partial<CreateVoiceRequest>
data: Partial<CreateVoiceRequest>,
): Promise<VoiceItem> => {
const response = await apiClient.patch(`/voices/${voiceId}`, data);
return response.data;
@@ -60,6 +60,6 @@ export const generateAIVoice = async (data: {
voice_type?: string;
speed?: number;
}): Promise<VoiceItem> => {
const response = await apiClient.post('/voices/generate', data);
const response = await apiClient.post("/voices/generate", data);
return response.data;
};