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
This commit is contained in:
2026-06-08 19:02:32 +08:00
parent a2741edf01
commit 59059caae7
6 changed files with 343 additions and 163 deletions

View File

@@ -24,9 +24,9 @@ import {
type CreatTaskRequestBody,
type ModelsListResponseBody,
type Banner,
type ModelCategory,
type ModelCategoryStringEnum,
} from '@/services/modules';
import {useAsyncData, useAsyncMutation} from './use-async';
import {useAsyncData, UseAsyncDataReturn, useAsyncMutation} from './use-async';
// ============================================================
// Banner
@@ -47,53 +47,38 @@ export function useBannerList() {
// 模型
// ============================================================
/** 模型列表数据 Hook按类别分别请求后合并) */
/** 模型列表数据 Hook单次请求全量模型 + 客户端按 category_name 分组)
* 因后端 category 查询参数实际不可用(传参返回空列表),
* 改为不带分类参数一次拉取全部,再由客户端根据响应中的
* category_name 字段手动分组。 */
export function useModelList() {
const {isLoggedIn} = useAppContext();
// 使用单个 fetcher 返回合并后的结果
const result = useAsyncData<ModelsListResponseBody[]>(
async () => {
const categories: ModelCategory[] = ["image_to_image", "text_to_image", "image_to_video", "text_to_audio"];
const results = await Promise.allSettled(
categories.map((category) =>
ModelAPI.fetchModels({
provider_name: '',
category,
page: 1,
page_size: 50,
}),
),
);
const allModels: ModelsListResponseBody[] = [];
results.forEach((r) => {
if (r.status === 'fulfilled') {
allModels.push(...r.value);
}
});
// 按优先级降序排列
allModels.sort((a, b) => b.priority - a.priority);
return allModels;
},
const result: UseAsyncDataReturn<ModelsListResponseBody[]> = useAsyncData<ModelsListResponseBody[]>(
() => ModelAPI.fetchModels({page: 1, page_size: 200}),
[],
{enabled: isLoggedIn, label: 'model-list'},
);
// 按类别分组
const groupedModels = useMemo(() => {
// 按 category_name 分组(后端返回的 category_name 为 slug 格式如 img2img
const groupedModels: Map<string, ModelsListResponseBody[]> = useMemo(() => {
const groups = new Map<string, ModelsListResponseBody[]>();
result.data?.forEach((model) => {
const cat = model.category_name || 'other';
result.data?.forEach((model: ModelsListResponseBody) => {
const cat: ModelCategoryStringEnum = model.model_type;
if (!groups.has(cat)) groups.set(cat, []);
groups.get(cat)!.push(model);
});
// 每组内按优先级降序排列
// groups.forEach((models) => {
// models.sort((a, b) => b.priority - a.priority);
// });
return groups;
}, [result.data]);
return {
...result,
/** 按类别分组后的模型 */
/** 按 model_type 分组后的模型key 为后端 slug */
groupedModels,
};
}
@@ -163,4 +148,4 @@ export function useSubmitTask(options?: {
// ---------- 重新导出类型和工具 ----------
export {MODEL_CATEGORY_LABELS};
export type {ModelCategory, ModelsListResponseBody, TaskInfoItemResponseBody, Banner};
export type {ModelCategoryStringEnum, ModelsListResponseBody, TaskInfoItemResponseBody, Banner};