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
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:
+22
-22
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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: "系统自动创建的默认项目",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 || [];
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user