feat(web): T-WEB-008/009 管理端界面(歌曲/模型/动作/审核 + VRM 预览)

This commit is contained in:
2026-09-14 19:00:55 +08:00
parent 0f66b53480
commit 640736c77b
30 changed files with 6704 additions and 0 deletions
+230
View File
@@ -0,0 +1,230 @@
/** 契约端点封装:页面只调用这些函数,不直接拼 URL。 */
import { apiDelete, apiGet, apiPatch, apiPost, apiUpload } from './client';
import type {
ContractVersion,
Health,
Model,
ModelPage,
ModelUpdate,
Motion,
MotionPage,
MotionUpdate,
PendingPage,
PublishStatus,
ReviewActionBody,
ReviewPage,
Song,
SongCreate,
SongPage,
SongUpdate,
TargetType,
} from './types';
/* ------------------------------------------------------------------ 列表 */
export interface ListParams {
page: number;
pageSize: number;
}
export function listSongs(
params: ListParams & { q?: string; status?: PublishStatus | ''; artist?: string },
): Promise<SongPage> {
return apiGet<SongPage>('/api/songs', {
page: params.page,
pageSize: params.pageSize,
q: params.q,
status: params.status,
artist: params.artist,
});
}
export function listMotions(
params: ListParams & {
q?: string;
songId?: number | '';
format?: string;
status?: PublishStatus | '';
},
): Promise<MotionPage> {
return apiGet<MotionPage>('/api/motions', {
page: params.page,
pageSize: params.pageSize,
q: params.q,
songId: params.songId,
format: params.format,
status: params.status,
});
}
export function listModels(
params: ListParams & { q?: string; format?: string; status?: PublishStatus | '' },
): Promise<ModelPage> {
return apiGet<ModelPage>('/api/models', {
page: params.page,
pageSize: params.pageSize,
q: params.q,
format: params.format,
status: params.status,
});
}
/* ------------------------------------------------------------------ 曲目 */
export function createSong(payload: SongCreate): Promise<Song> {
return apiPost<Song>('/api/songs', payload);
}
export function updateSong(id: number, payload: SongUpdate): Promise<Song> {
return apiPatch<Song>(`/api/songs/${id}`, payload);
}
export function deleteSong(id: number): Promise<void> {
return apiDelete(`/api/songs/${id}`);
}
/** multipart 字段名固定为 file。 */
export function uploadSongAudio(id: number, file: File): Promise<Song> {
const form = new FormData();
form.append('file', file);
return apiUpload<Song>(`/api/songs/${id}/audio`, form);
}
/* ------------------------------------------------------------------ 动作 */
export interface MotionUploadInput {
file: File;
name: string;
songId?: number;
fps?: number;
autoPreprocess: boolean;
status: PublishStatus;
sourceType: string;
sourceName: string;
sourceUrl: string;
license: string;
author: string;
}
export function uploadMotion(input: MotionUploadInput): Promise<Motion> {
const form = new FormData();
form.append('file', input.file);
form.append('name', input.name);
if (input.songId !== undefined) form.append('songId', String(input.songId));
if (input.fps !== undefined) form.append('fps', String(input.fps));
form.append('autoPreprocess', input.autoPreprocess ? 'true' : 'false');
form.append('status', input.status);
form.append('sourceType', input.sourceType);
form.append('sourceName', input.sourceName);
form.append('sourceUrl', input.sourceUrl);
form.append('license', input.license);
form.append('author', input.author);
return apiUpload<Motion>('/api/motions', form);
}
export function updateMotion(id: number, payload: MotionUpdate): Promise<Motion> {
return apiPatch<Motion>(`/api/motions/${id}`, payload);
}
export function deleteMotion(id: number): Promise<void> {
return apiDelete(`/api/motions/${id}`);
}
export function preprocessMotion(id: number): Promise<Motion> {
return apiPost<Motion>(`/api/motions/${id}/preprocess`, {});
}
/* ------------------------------------------------------------------ 模型 */
export interface ModelUploadInput {
file: File;
name: string;
isPrimary: boolean;
status: PublishStatus;
sourceType: string;
sourceName: string;
sourceUrl: string;
license: string;
author: string;
}
export function uploadModel(input: ModelUploadInput): Promise<Model> {
const form = new FormData();
form.append('file', input.file);
form.append('name', input.name);
form.append('isPrimary', input.isPrimary ? 'true' : 'false');
form.append('status', input.status);
form.append('sourceType', input.sourceType);
form.append('sourceName', input.sourceName);
form.append('sourceUrl', input.sourceUrl);
form.append('license', input.license);
form.append('author', input.author);
return apiUpload<Model>('/api/models', form);
}
export function updateModel(id: number, payload: ModelUpdate): Promise<Model> {
return apiPatch<Model>(`/api/models/${id}`, payload);
}
export function deleteModel(id: number): Promise<void> {
return apiDelete(`/api/models/${id}`);
}
export function setPrimaryModel(id: number): Promise<Model> {
return apiPost<Model>(`/api/models/${id}/primary`, {});
}
/* ------------------------------------------------------------------ 审核 */
export function listPending(params: ListParams & { status: PublishStatus }): Promise<PendingPage> {
return apiGet<PendingPage>('/api/reviews', {
status: params.status,
page: params.page,
pageSize: params.pageSize,
});
}
export function listReviewRecords(
params: ListParams & { targetType?: TargetType | '' },
): Promise<ReviewPage> {
return apiGet<ReviewPage>('/api/reviews', {
targetType: params.targetType,
page: params.page,
pageSize: params.pageSize,
});
}
export function reviewApprove(
targetType: TargetType,
id: number,
body: ReviewActionBody,
): Promise<void> {
return apiPost<void>(`/api/reviews/${targetType}/${id}/approve`, body);
}
export function reviewReject(
targetType: TargetType,
id: number,
body: ReviewActionBody,
): Promise<void> {
return apiPost<void>(`/api/reviews/${targetType}/${id}/reject`, body);
}
export function reviewSubmit(
targetType: TargetType,
id: number,
body: ReviewActionBody,
): Promise<void> {
return apiPost<void>(`/api/reviews/${targetType}/${id}/submit`, body);
}
/* -------------------------------------------------------------- 目录/健康 */
export function getHealth(): Promise<Health> {
return apiGet<Health>('/health');
}
export function getCatalogVersion(): Promise<ContractVersion> {
return apiGet<ContractVersion>('/api/catalog/version');
}