refactor: 消除全部 Record<string, unknown>,TypeScript 严格类型治理 #331

Merged
xiaoxia merged 5 commits from fix/frontend-type-debt-round2 into develop 2026-07-14 21:15:56 +08:00
11 changed files with 140 additions and 47 deletions
+1 -1
View File
@@ -204,7 +204,7 @@ export const createAsset = async (data: {
/** 更新素材(名称、metadata 等) */
export const updateAsset = async (
assetId: string,
data: { name?: string; metadata?: Record<string, unknown> },
data: { name?: string; metadata?: AssetMetadata },
): Promise<AssetItem> => {
const response = await apiClient.put(`/assets/${assetId}`, data);
return response.data;
+2 -1
View File
@@ -129,7 +129,8 @@ apiClient.interceptors.response.use(
const safeExtractString = (val: unknown): string => {
if (typeof val === "string") return val;
if (typeof val === "object" && val !== null) {
const obj = val as Record<string, unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取,后端错误格式不固定
const obj = val as Record<string, any>;
if (typeof obj.message === "string") return obj.message;
if (typeof obj.msg === "string") return obj.msg;
if (typeof obj.detail === "string") return obj.detail;
+36 -8
View File
@@ -97,6 +97,30 @@ export interface EditPlanConfig {
green_screen_config?: ChromaKeyConfig;
sticker_config?: StickerConfig;
cover_config?: CoverConfig;
/** 前端扩展:关联的素材 ID 列表 */
asset_ids?: string[];
/** 配音 ID */
voice_id?: string;
/** 克隆音色档案 ID */
voice_clone_profile_id?: string;
/** 自定义配音音频 URL */
custom_audio_url?: string;
/** 自定义配音文本 */
custom_text?: string;
/** 视频比例 */
ratio?: string;
/** 视频风格 */
style?: string;
/** 目标时长(秒) */
duration?: number;
/** 是否自动生成字幕 */
auto_subtitles?: boolean;
/** 是否启用 BGM */
bgm?: boolean;
/** 生成数量 */
generate_count?: number;
/** 素材模式 */
material_mode?: string;
}
/** 剪辑计划(后端响应) */
@@ -170,7 +194,7 @@ export interface GenerationStatusResponse {
export interface GeneratedVideo {
id: string;
project_id?: string;
generation_task_id: string;
generation_task_id?: string;
name: string;
file_url: string;
file_size?: number;
@@ -182,6 +206,8 @@ export interface GeneratedVideo {
status: string;
review_status?: string;
download_url?: string;
created_at?: string;
updated_at?: string;
}
/* ============================================================
@@ -226,13 +252,15 @@ export interface GenerateCoverRequest {
/** AI 封面生成响应 */
export interface GenerateCoverResponse {
plan_id: string;
cover: {
scheme?: string;
asset_id?: string;
frame_time?: number;
thumbnail_url?: string;
[key: string]: unknown;
};
cover: CoverResult;
}
/** 封面生成结果 */
export interface CoverResult {
scheme?: string;
asset_id?: string;
frame_time?: number;
thumbnail_url?: string;
}
/* ============================================================
+13 -1
View File
@@ -139,11 +139,23 @@ export interface GenerateFromTemplatePayload {
voiceover_duration: number;
}
/** 验证警告详情 */
export interface ValidationWarningDetails {
/** 相关字段名 */
field?: string;
/** 期望值 */
expected?: string | number;
/** 实际值 */
actual?: string | number;
/** 建议值 */
suggested?: string | number;
}
/** 验证/生成响应 */
export interface ValidateWarning {
code: string;
message: string;
details?: Record<string, unknown>;
details?: ValidationWarningDetails;
}
/** 使用模板生成响应 */
+14 -21
View File
@@ -60,33 +60,26 @@ export interface BatchDownloadStatus {
/**
* 将 generation task 数据映射为 ProductItem 格式
*/
function mapTaskToProductItem(
task: GeneratedVideo | Record<string, unknown>,
): ProductItem {
const video = task as GeneratedVideo;
function mapTaskToProductItem(task: GeneratedVideo): ProductItem {
return {
id: video.id,
title: video.name || "未命名视频",
video_url: video.file_url,
thumbnail_url: video.thumbnail_url,
duration_seconds: video.duration,
file_size: video.file_size,
id: task.id,
title: task.name || "未命名视频",
video_url: task.file_url,
thumbnail_url: task.thumbnail_url,
duration_seconds: task.duration,
file_size: task.file_size,
resolution:
video.width && video.height
? `${video.width}x${video.height}`
: undefined,
task.width && task.height ? `${task.width}x${task.height}` : undefined,
status:
video.status === "completed"
task.status === "completed"
? "completed"
: video.status === "failed"
: task.status === "failed"
? "failed"
: "processing",
review_status: video.review_status as ReviewStatus | undefined,
project_id: video.project_id,
created_at: (task as Record<string, unknown>).created_at as
string | undefined,
updated_at: (task as Record<string, unknown>).updated_at as
string | undefined,
review_status: task.review_status as ReviewStatus | undefined,
project_id: task.project_id,
created_at: task.created_at,
updated_at: task.updated_at,
};
}
+14 -2
View File
@@ -8,6 +8,18 @@ import apiClient from "./client";
/* ── 类型定义 ──────────────────────────────────── */
/** TTS 元数据(合成时附带的扩展信息) */
export interface TTSMetadata {
/** 语音时长(秒) */
duration?: number;
/** 采样率(Hz */
sample_rate?: number;
/** 语言 */
language?: string;
/** 其他扩展字段 */
[key: string]: unknown;
}
/** TTS 合成请求参数 */
export interface TTSSynthesizeRequest {
text: string;
@@ -18,7 +30,7 @@ export interface TTSSynthesizeRequest {
voice_model?: string;
voice_clone_profile_id?: string;
format?: string;
metadata?: Record<string, unknown>;
metadata?: TTSMetadata;
}
/** TTS 合成创建响应 */
@@ -49,7 +61,7 @@ export interface TTSJob {
error_message: string | null;
retry_count: number;
max_retries: number;
metadata_: Record<string, unknown> | null;
metadata_: TTSMetadata | null;
created_at: string;
updated_at: string;
}
+14 -2
View File
@@ -36,6 +36,18 @@ export interface CreateVoiceCloneRequest {
/* ── 后端 API 类型 ────────────────────────────────────── */
/** 音色克隆元数据(克隆时附带的扩展信息) */
export interface VoiceCloneMetadata {
/** 语音时长(秒) */
duration?: number;
/** 采样率(Hz */
sample_rate?: number;
/** 音色 ID(克隆完成后分配) */
voice_id?: string;
/** 其他扩展字段 */
[key: string]: unknown;
}
/** 后端克隆档案响应 */
export interface VoiceCloneProfile {
id: string;
@@ -51,7 +63,7 @@ export interface VoiceCloneProfile {
error_message: string | null;
retry_count: number;
max_retries: number;
metadata_: Record<string, unknown> | null;
metadata_: VoiceCloneMetadata | null;
created_at: string;
updated_at: string;
}
@@ -80,7 +92,7 @@ export interface CreateVoiceCloneRequestFull {
language?: string;
gender?: string;
max_retries?: number;
metadata_?: Record<string, unknown>;
metadata_?: VoiceCloneMetadata;
}
/* ── 辅助函数 ─────────────────────────────────────────── */
+16 -7
View File
@@ -202,8 +202,8 @@ const GeneratePage: React.FC = () => {
try {
const plan = await getEditPlan(editPlanId);
if (plan.name) setTitle(plan.name);
const cfg = plan.config as Record<string, unknown>;
if (cfg && Array.isArray(cfg.asset_ids)) {
const cfg = plan.config;
if (cfg?.asset_ids) {
setSelectedMaterials(
cfg.asset_ids.filter((v): v is string => typeof v === "string"),
);
@@ -477,7 +477,13 @@ const GeneratePage: React.FC = () => {
setGenerateError(null);
try {
const voiceConfig: Record<string, unknown> = {};
const voiceConfig: Pick<
EditPlanConfig,
| "voice_id"
| "voice_clone_profile_id"
| "custom_audio_url"
| "custom_text"
> = {};
if (voiceMode === "preset") {
voiceConfig.voice_id = selectedVoice || undefined;
} else if (voiceMode === "clone") {
@@ -502,7 +508,7 @@ const GeneratePage: React.FC = () => {
bgm,
generate_count: generateCount,
material_mode: materialMode,
} as EditPlanConfig,
},
total_duration: duration,
source_edit_plan_id: editPlanId || undefined,
});
@@ -561,7 +567,8 @@ const GeneratePage: React.FC = () => {
const safeExtract = (val: unknown): string => {
if (typeof val === "string") return val;
if (typeof val === "object" && val !== null) {
const obj = val as Record<string, unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取,后端错误格式不固定
const obj = val as Record<string, any>;
if (typeof obj.message === "string") return obj.message;
if (typeof obj.msg === "string") return obj.msg;
if (typeof obj.detail === "string") return obj.detail;
@@ -621,7 +628,8 @@ const GeneratePage: React.FC = () => {
const extractString = (val: unknown): string => {
if (typeof val === "string") return val;
if (typeof val === "object" && val !== null) {
const obj = val as Record<string, unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取,后端错误格式不固定
const obj = val as Record<string, any>;
if (typeof obj.message === "string") return obj.message;
if (typeof obj.msg === "string") return obj.msg;
if (typeof obj.detail === "string") return obj.detail;
@@ -651,7 +659,8 @@ const GeneratePage: React.FC = () => {
const safeExtractErr = (val: unknown): string => {
if (typeof val === "string") return val;
if (typeof val === "object" && val !== null) {
const obj = val as Record<string, unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取,后端错误格式不固定
const obj = val as Record<string, any>;
if (typeof obj.message === "string") return obj.message;
if (typeof obj.msg === "string") return obj.msg;
if (typeof obj.detail === "string") return obj.detail;
@@ -99,10 +99,20 @@ const formatDuration = (seconds: number): string => {
return `${m}${s > 0 ? `${s}` : ""}`;
};
/** 配置展示字段(formatConfig 提取通用配置的可读属性) */
interface ConfigDisplayFields {
font_size?: string | number;
font_family?: string;
color?: string;
position?: string;
volume?: string | number;
name?: string;
}
/** 格式化配置对象为可读文本 */
const formatConfig = (config?: object): string => {
if (!config || Object.keys(config).length === 0) return "默认";
const c = config as Record<string, unknown>;
const c = config as ConfigDisplayFields;
const parts: string[] = [];
if (c.font_size) parts.push(`字号: ${c.font_size}`);
if (c.font_family) parts.push(`字体: ${c.font_family}`);
@@ -130,12 +130,20 @@ const mapAssetToMaterial = (asset: AssetItem): VoiceMaterial => {
};
};
/** 配音素材上传元数据(传递给 createAsset 的 metadata */
interface VoiceAssetMetadata {
gender: VoiceGender;
description: string;
duration: number;
[key: string]: unknown;
}
/** 前端表单数据 → 后端 metadata(标签走独立 API,不再写 metadata.style */
const buildMetadata = (data: {
gender: VoiceGender;
description: string;
duration?: number;
}): Record<string, unknown> => ({
}): VoiceAssetMetadata => ({
gender: data.gender,
description: data.description,
duration: data.duration || 0,
+10 -2
View File
@@ -623,12 +623,20 @@ const getAudioDuration = (file: File): Promise<number> =>
audio.src = url;
});
/** 音色上传元数据(传递给 createAsset 的 metadata */
interface VoiceUploadMetadata {
gender?: string;
description?: string;
duration?: number;
[key: string]: unknown;
}
const buildVoiceMetadata = (data: {
gender?: string;
description?: string;
duration?: number;
}): Record<string, unknown> => {
const metadata: Record<string, unknown> = {};
}): VoiceUploadMetadata => {
const metadata: VoiceUploadMetadata = {};
if (data.gender) metadata.gender = data.gender;
if (data.description) metadata.description = data.description;
if (data.duration) metadata.duration = Math.round(data.duration);