feat(ui): 新建我的音色页面
Deploy / Deploy Staging (push) Failing after 194h34m44s
CI/CD Pipeline / Frontend Lint (push) Failing after 194h35m24s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 194h35m35s
Deploy / Production Browser E2E (push) Failing after 1853h9m36s
Deploy / Build Production Runtime Images (push) Failing after 1853h9m40s
Deploy / Deploy Production (push) Failing after 1853h40m59s

- 新增 api/voiceClone.ts: mock 数据模块(4条音色,含 ready/processing 状态)
- 新增 pages/voice-clone/VoiceClone.tsx: 主页面,卡片网格+空状态+编辑弹窗+toast
- 新增 pages/voice-clone/voice-clone.css: V21 设计系统样式
- 更新路由 /voice-clone、侧边栏菜单、Header导航、PageHead面包屑
- 零 antd 依赖,使用 V21 UI 组件(Button, Tooltip)
- TypeScript 检查通过
This commit is contained in:
灵应
2026-07-01 10:18:54 +08:00
parent 32bf71a9c3
commit 3a6d8d4bfd
7 changed files with 834 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
/**
* 音色克隆 API
* Sprint 4 — mock 数据,后续迁移真实 API
*/
/** 克隆音色状态 */
export type VoiceCloneStatus = "ready" | "processing" | "failed";
/** 克隆音色条目 */
export interface VoiceClone {
id: string;
name: string;
duration_seconds: number;
status: VoiceCloneStatus;
sample_url?: string;
created_at: string;
updated_at: string;
}
/** 创建克隆请求 */
export interface CreateVoiceCloneRequest {
name: string;
audio_url: string;
}
/* ── Mock 数据 ─────────────────────────────────────────── */
const MOCK_CLONES: VoiceClone[] = [
{
id: "vc-001",
name: "我的播音腔",
duration_seconds: 128,
status: "ready",
sample_url: "",
created_at: "2026-06-20T10:30:00Z",
updated_at: "2026-06-20T10:35:00Z",
},
{
id: "vc-002",
name: "温柔女声",
duration_seconds: 256,
status: "ready",
sample_url: "",
created_at: "2026-06-22T14:00:00Z",
updated_at: "2026-06-22T14:08:00Z",
},
{
id: "vc-003",
name: "磁性男声",
duration_seconds: 95,
status: "processing",
created_at: "2026-06-28T09:15:00Z",
updated_at: "2026-06-28T09:15:00Z",
},
{
id: "vc-004",
name: "童声模仿",
duration_seconds: 60,
status: "ready",
sample_url: "",
created_at: "2026-06-25T16:45:00Z",
updated_at: "2026-06-25T16:50:00Z",
},
];
/* ── 辅助函数 ─────────────────────────────────────────── */
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
/** 格式化时长 */
export const formatDuration = (seconds: number): string => {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m}:${String(s).padStart(2, "0")}`;
};
/* ── API 函数 ─────────────────────────────────────────── */
/** 获取所有克隆音色 */
export const getVoiceClones = async (): Promise<VoiceClone[]> => {
await delay(300);
return [...MOCK_CLONES];
};
/** 获取单个克隆音色详情 */
export const getVoiceCloneDetail = async (
id: string,
): Promise<VoiceClone | null> => {
await delay(200);
return MOCK_CLONES.find((v) => v.id === id) ?? null;
};
/** 创建克隆音色 */
export const createVoiceClone = async (
data: CreateVoiceCloneRequest,
): Promise<VoiceClone> => {
await delay(500);
const newClone: VoiceClone = {
id: `vc-${Date.now()}`,
name: data.name,
duration_seconds: 0,
status: "processing",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
MOCK_CLONES.unshift(newClone);
return newClone;
};
/** 删除克隆音色 */
export const deleteVoiceClone = async (id: string): Promise<void> => {
await delay(300);
const idx = MOCK_CLONES.findIndex((v) => v.id === id);
if (idx >= 0) MOCK_CLONES.splice(idx, 1);
};
/** 更新克隆音色名称 */
export const updateVoiceClone = async (
id: string,
data: Partial<Pick<VoiceClone, "name">>,
): Promise<VoiceClone> => {
await delay(300);
const clone = MOCK_CLONES.find((v) => v.id === id);
if (!clone) throw new Error("音色不存在");
Object.assign(clone, data, { updated_at: new Date().toISOString() });
return { ...clone };
};