feat: 实现业务首页(三栏布局 + 轮播图)

## 首页整体架构
  - 新增 src/pages/home/ — 完整首页模块(10 个组件)
    - HomePage / HomeContent:三栏弹性布局,拖动分隔条调整列宽
    - BannerCarousel:顶部轮播图(antd Carousel,2280×90 长条图片适配)
    - LeftPanel:模型选择器(Menu 分组) + 任务记录(Table 分页),纵向拖拽调整比例
    - CenterPanel / ModelInputForm:动态表单,根据模型 param_schema/ui_config 渲染控件
    - RightPanel / OutputPreview:任务输出预览(Image/Video)
  - 新增 Layout 组件:h-screen flex 布局,首页 Banner + NavBar + Outlet

  ## 数据层(数据与视图分离)
  - 新增 src/services/modules/ — API 模块(banner/models/task/auth)
    - TaskAPI 静态方法,TaskInfoItemResponseBody 判别联合类型
    - ModelAPI / BannerAPI,const 断言路由常量
  - 新增 src/hooks/use-async.ts — 通用异步 Hook
    - useAsyncData:竞态处理、条件请求(enabled)、自动错误日志
    - useAsyncMutation:数据变更,onSuccess/onError
    - getErrorMessage():RequestErrorType → 可读错误信息
  - 新增 src/hooks/use-api.ts — 业务 Hook
    - useBannerList / useModelList / useTaskList / useTaskDetail / useSubmitTask

  ## 主题与交互
  - 重写暗色令牌:科技感蓝紫(#080C24 底 / #0F1340 面板 / #2A3278 边框)
  - 三栏分隔线:token.colorBorderSecondary 常驻 + 手柄竖线 hover 高亮
  - 左面板纵向拖拽:模型区/任务区可调比例,ResizeObserver 自适应
  - 暗色/亮色同步:tokens.ts + antd-theme.ts + globals.css 三处一致

  ## 基础设施
  - EventBus 新增 TASK_SUBMITTED 事件(替代 Context 数字递增 hack)
  - HomeContext 状态管理(选中模型、任务分页、折叠状态)
  - 所有组件边框使用 token.colorBorderSecondary(主题自适应)
  - Table 滚动高度 ResizeObserver 动态计算(修复分页器遮挡)
This commit is contained in:
2026-06-05 16:24:17 +08:00
parent 6204c14b88
commit 1da95eaef3
31 changed files with 2639 additions and 470 deletions

View File

@@ -0,0 +1,232 @@
// ============================================================
// 任务模块 — 任务记录 CRUD API
// GET /api/v1/tasks → 任务列表(分页/游标)
// POST /api/v1/tasks → 提交任务
// GET /api/v1/tasks/:id → 任务详情
// GET /api/v1/tasks/:id/status → 轻量轮询状态
// GET /api/v1/tasks/export → 导出 CSV
//
// TaskAPI 的静态方法直接返回 get/post 的 Promise<T>
// 不做额外的 async/await 包装,由调用方决定是否 await。
// ============================================================
import {get, post} from '../request';
// ============================================================
// 基础枚举 / 字面量类型
// ============================================================
/** 输出类型映射 */
export enum OutputTypeMap {
video = '图片',
image = '视频',
audio = '音频',
}
export type OutputTypeString = keyof typeof OutputTypeMap;
/** 任务状态 */
export enum TaskStatusMap {
pending = '待处理',
submitted = '已提交',
processing = '处理中',
success = '成功',
failed = '失败',
}
export type TaskStatusString = keyof typeof TaskStatusMap;
// ============================================================
// 请求体 / 参数类型
// ============================================================
/** 任务列表请求参数 */
export interface TaskListRequestParams {
status?: TaskStatusString;
user_ids?: string[];
model_ids?: string[];
/** 游标分页:上次最后一条的 created_atISO 8601 */
cursor?: string;
/** 页码OFFSET 分页模式default: 1 */
page?: number;
/** 每页条数default: 20 */
page_size?: number;
}
/** 创建任务请求体 */
export interface CreatTaskRequestBody {
model_id: string;
params: Record<string, string>;
device_id?: string;
tags?: string[];
}
/** 导出 CSV 参数 */
export interface ExportTaskCSVParams {
start_date: Date;
end_date: Date;
status?: TaskStatusString;
user_ids?: string[];
model_ids?: string[];
}
// ============================================================
// 内容项 — 带判别标签的联合类型
// ============================================================
interface BaseContentItem {
type: string;
}
interface TextContent extends BaseContentItem {
type: 'text';
text: string;
}
interface ImageUrlContent extends BaseContentItem {
type: 'image_url';
role: 'reference_image';
image_url: { url: string };
}
interface VideoUrlContent extends BaseContentItem {
type: 'video_url';
role: 'reference_video';
video_url: { url: string };
}
interface AudioContent extends BaseContentItem {
type: 'audio_url';
role: 'reference_audio';
audio_url: { url: string };
}
export type ContentItem =
| TextContent
| ImageUrlContent
| VideoUrlContent
| AudioContent;
// ============================================================
// 各输出类型的 UI 参数
// ============================================================
export interface ImageUiParams {
imageUrls?: string[];
prompt: string;
aspectRatio: string;
resolution?: string;
}
export interface VideoUiParams {
resolution?: string;
ratio?: string;
model?: string;
generate_audio: boolean;
duration: number;
content: ContentItem[];
}
export interface AudioUiParams {
format?: string;
sampleRate?: number;
bitrate?: number;
}
// ============================================================
// 响应体 — 用判别联合按 output_type 分发
// ============================================================
/** 任务条目基类(不含 output_type 特有字段) */
interface BaseTaskItemResponseBody {
id: string;
user_id: string;
model_id: string;
model_name: string;
provider_name: string;
status: TaskStatusString;
external_task_id: string;
cost_cent: number;
estimated_cost_cent: number;
error_code: string;
poll_attempts: number;
is_favorite: boolean;
submitted_at: Date;
finished_at: Date;
created_at: Date;
updated_at: Date;
prompt?: string;
output_assets?: Array<{ url: string }>;
tags: Array<string | null>;
error_message: string;
user_display_name: string | null;
}
/** 判别联合:根据 output_type 收窄 ui_params */
export type TaskInfoItemResponseBody =
| (BaseTaskItemResponseBody & { output_type: 'image'; ui_params: ImageUiParams })
| (BaseTaskItemResponseBody & { output_type: 'video'; ui_params: VideoUiParams })
| (BaseTaskItemResponseBody & { output_type: 'audio'; ui_params: AudioUiParams });
/** 任务列表分页响应 */
export interface TaskInfoDataResponseBody {
has_more: boolean;
next_cursor: string;
page: number;
page_size: number;
total: number;
items: TaskInfoItemResponseBody[];
}
/** 轻量轮询状态响应 */
export interface TaskStatusResponseBody {
task_id: string;
status: TaskStatusString;
progress: number;
output_assets?: string[];
error_code?: string;
error_message?: string;
cost_cent?: number;
}
// ============================================================
// 路由常量
// ============================================================
const TaskUrlObj = {
task: '/api/v1/tasks',
taskInfo: (task_id: string) => `/api/v1/tasks/${task_id}`,
taskStatus: (task_id: string) => `/api/v1/tasks/${task_id}/status`,
tasksCSV: '/api/v1/tasks/export',
} as const;
// ============================================================
// TaskAPI — 静态方法直接返回 Promise<T>,不做二次 async 包装
// ============================================================
export class TaskAPI {
/** 获取任务列表(分页/游标) */
static getTaskList(params: TaskListRequestParams): Promise<TaskInfoDataResponseBody> {
return get<TaskInfoDataResponseBody>(TaskUrlObj.task, params as Record<string, unknown>);
}
/** 提交新任务 */
static submitTask(body: CreatTaskRequestBody): Promise<TaskInfoItemResponseBody> {
return post<TaskInfoItemResponseBody>(TaskUrlObj.task, body);
}
/** 获取任务详情 */
static getTaskInfo(task_id: string): Promise<TaskInfoItemResponseBody> {
return get<TaskInfoItemResponseBody>(TaskUrlObj.taskInfo(task_id));
}
/** 轻量轮询任务状态 */
static getTaskStatus(task_id: string): Promise<TaskStatusResponseBody> {
return get<TaskStatusResponseBody>(TaskUrlObj.taskStatus(task_id));
}
/** 导出任务 CSV */
static exportTaskCSV(params: ExportTaskCSVParams): Promise<File> {
return get<File>(TaskUrlObj.tasksCSV, params as unknown as Record<string, unknown>);
}
}