Files
ele-HeiXiu/src/services/modules/models.ts
YoungestSongMo 59059caae7 feat: 重构模型列表UI + 修复后端数据格式适配
模型列表 UI 重构:
- ModelSelector 从 Menu 折叠分组改为 Tabs 标签页 + 平铺列表
- 新增「全部模型」Tab + 各分类由 MODEL_CATEGORIES 数组控制顺序
- ModelItem 新增 hover 高亮效果 + 维护中模型禁用 + toast 提示
- 新增错误态(Result + 重试按钮)

后端数据格式适配:
- param_schema 类型从 string[] 改为 ParamSchemaItem[]({id, map_to, spec, ui})
- 修复 Form.Item 重复 key 报错(item.id 作 React key,item.map_to 作表单字段名)
- ModelsListResponseBody 字段类型收紧(model_type / category_name / quota_config)

数据层重构:
- useModelList 从 4 次并行请求改为 1 次全量拉取(后端 category 参数不可用)
- 分组键从 category_name 改为 model_type
- page_size 提升至 200

类型体系增强:
- type 别名改为 as const 数组推导(MODEL_CATEGORIES 单一数据源)
- 新增 CATEGORY_SLUG_MAP + resolveCategoryLabel + toCategorySlug 映射层
- 类型重命名 ModelCategory → ModelCategoryStringEnum
2026-06-08 19:02:32 +08:00

159 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {get} from '../request';
// 注意:后端 /api/v1/models 的 category 查询参数实际不可用(传参返回空列表),
// 因此客户端采用"全量拉取 + 按 category_name 字段分组"的策略。
// 后端分类 slug 格式为 img2img / txt2img 等,前端通过 CATEGORY_SLUG_MAP 映射。
// ---------- 路由常量 ----------
const ModelsUrlObj = {
modelList: '/api/v1/models',
getModelInfo: '/api/v1/models/{model_id}',
estimateCost: '/api/v1/models/{model_id}/estimate'
} as const;
// ---------- 类型 ----------
/** 模型类别常量数组(单一数据源,类型由此推导) */
export const MODEL_CATEGORIES = [
'image_to_image',
'text_to_image',
'image_to_video',
'text_to_audio',
] as const;
export type ModelCategoryStringEnum = typeof MODEL_CATEGORIES[number];
/**
* 模型参数 Schema 项
*
* 后端 /api/v1/models 返回的 param_schema 为对象数组,
* 每项描述一个模型入参的元信息标识、字段映射、规格约束、UI 配置)。
*/
export interface ParamSchemaItem {
id: string;
/** 映射到提交参数中的字段名 */
map_to: string;
/** 规格约束(类型、范围、默认值等) */
spec: Record<string, unknown>;
/** UI 控件配置 */
ui: Record<string, unknown>;
}
/** 模型类别中文映射 */
export const MODEL_CATEGORY_LABELS: Record<ModelCategoryStringEnum, string> = {
image_to_image: '图生图',
text_to_image: '文生图',
image_to_video: '图生视频',
text_to_audio: '文生音频',
};
/**
* 前端 ModelCategory → 后端分类 slug 映射
*
* 后端分类 API 使用简写 slug如 img2img而非前端的长标识符。
* 查询模型列表时需将前端类别转为后端 slug 作为 category 参数。
*/
export const CATEGORY_SLUG_MAP: Record<ModelCategoryStringEnum, string> = {
image_to_image: 'img2img',
text_to_image: 'txt2img',
image_to_video: 'img2video',
text_to_audio: 'txt2audio',
};
/** 后端 slug → 前端 ModelCategory 反向映射(由 CATEGORY_SLUG_MAP 自动推导) */
const SLUG_TO_CATEGORY: Record<string, ModelCategoryStringEnum> = Object.fromEntries(
Object.entries(CATEGORY_SLUG_MAP).map(([cat, slug]) => [slug, cat as ModelCategoryStringEnum]),
) as Record<string, ModelCategoryStringEnum>;
/**
* 根据任意 category_name 值解析出最佳中文标签
*
* 解析优先级:
* 1. 匹配 MODEL_CATEGORY_LABELS前端 ModelCategory 字面量)
* 2. 匹配 SLUG_TO_CATEGORY 反向映射(后端 slug → 中英文标签)
* 3. 兜底返回原始字符串
*/
export function resolveCategoryLabel(categoryName: string): string {
// 优先按 ModelCategory 字面量查找
if (categoryName in MODEL_CATEGORY_LABELS) {
return MODEL_CATEGORY_LABELS[categoryName as ModelCategoryStringEnum];
}
// 按后端 slug 反向查找
const cat = SLUG_TO_CATEGORY[categoryName];
if (cat) {
return MODEL_CATEGORY_LABELS[cat];
}
// 兜底返回原始值
return categoryName;
}
/**
* 将前端 ModelCategory 转为后端 API 用的分类 slug
*/
export function toCategorySlug(category: ModelCategoryStringEnum): string {
return CATEGORY_SLUG_MAP[category];
}
/**
* 查询可用模型的请求参数
*/
export interface ModelsListReqeustParams {
provider_name?: string;
category?: ModelCategoryStringEnum;
page: number;
page_size: number;
}
/**
* 查询可用模型的响应体
*/
export interface ModelsListResponseBody {
id: string;
name: string;
provider_name: string;
model_type: ModelCategoryStringEnum;
category_name: ModelCategoryStringEnum | null;
status: string;
priority: number;
response_mode: string;
pricing_config: Record<string, unknown>;
param_schema: ParamSchemaItem[];
quota_config: Record<string, unknown> | null;
ui_config: Record<string, unknown>;
meta_config: Record<string, unknown>;
constraints_config: Record<string, unknown>;
expires_at: string;
created_at: string;
updated_at: string;
}
/**
* 模型信息请求参数
*/
export interface ModelInfoRequestParams {
model_id: string;
}
/** 模型详情响应体(与列表项结构一致) */
export type ModelInfoResponseBody = ModelsListResponseBody;
// ---------- API ----------
export class ModelAPI {
/**
* 获取可用模型列表
* GET /api/v1/models
*/
static fetchModels(params: ModelsListReqeustParams): Promise<ModelsListResponseBody[]> {
return get<ModelsListResponseBody[]>(ModelsUrlObj.modelList, params as unknown as Record<string, unknown>);
}
/**
* 获取单个模型详情
* GET /api/v1/models/:id
*/
static fetchModelInfo(modelId: string): Promise<ModelInfoResponseBody> {
const url = ModelsUrlObj.getModelInfo.replace('{model_id}', modelId);
return get<ModelInfoResponseBody>(url);
}
}