feat: 前后端联调 — 任务创建 + 账户信息 + 预估花费 (0.0.18)
- task: device_id 改为必填参数,API 层 Omit 封装 + getDeviceId() 自动注入 - pricing.ts: Python 后端 calculate_cost 完整移植,六种定价模式 + 6 个 Bug 修复 (浮点冗余/维度匹配不一致/null 守卫/matrix 数据提取/unit_scale 等) - ModelInputForm: 三层兜底预估消费展示 + React Hooks 顺序修复 - AccountInfo: VIP 等级列表匹配,套餐详情展示(说明/周期/席位/到期) - 模型列表加密缓存 (safe-storage, stale-while-revalidate) - VIP API 模块 (激活码/等级/订单/模拟支付) - device_id 统一使用 UUID v4 持久化 (与 login/register 对齐)
This commit is contained in:
@@ -12,12 +12,13 @@
|
||||
// - 用户可读错误信息(errorMessage)
|
||||
// ============================================================
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {useMemo, useState} from 'react';
|
||||
import {useAppContext} from '@/components/AppProvider';
|
||||
import { TaskAPI, type TaskListRequestParams, type TaskInfoItemResponseBody, type CreatTaskRequestBody } from '@/services/modules/task';
|
||||
import { ModelAPI, MODEL_CATEGORY_LABELS, type ModelsListResponseBody, type ModelCategoryStringEnum } from '@/services/modules/models';
|
||||
import { fetchBanners, type Banner } from '@/services/modules/banner';
|
||||
import {useAsyncData, UseAsyncDataReturn, useAsyncMutation} from './use-async';
|
||||
import {getCachedModels, setCachedModels} from '@/services/cache/model-cache';
|
||||
|
||||
// ============================================================
|
||||
// Banner
|
||||
@@ -38,37 +39,63 @@ export function useBannerList() {
|
||||
// 模型
|
||||
// ============================================================
|
||||
|
||||
/** 模型列表数据 Hook(单次请求全量模型 + 客户端按 category_name 分组)
|
||||
/** 模块级:单次会话中是否已完成首次 API 拉取 */
|
||||
let modelFirstFetchDone = false;
|
||||
|
||||
/** 模型列表数据 Hook(缓存优先 + 后台刷新)
|
||||
*
|
||||
* 策略:
|
||||
* 1. 启动时同步读取加密缓存 → 立即展示(避免每次打开都 loading)
|
||||
* 2. 异步拉取最新模型列表 → 覆盖缓存数据
|
||||
* 3. 拉取成功后将最新数据加密写入 localStorage
|
||||
*
|
||||
* 因后端 category 查询参数实际不可用(传参返回空列表),
|
||||
* 改为不带分类参数一次拉取全部,再由客户端根据响应中的
|
||||
* category_name 字段手动分组。 */
|
||||
export function useModelList() {
|
||||
const {isLoggedIn} = useAppContext();
|
||||
|
||||
// 缓存即时数据(首次 render 同步读取,后续 API 返回后覆盖)
|
||||
const [instantData, setInstantData] = useState<ModelsListResponseBody[] | null>(() =>
|
||||
getCachedModels(),
|
||||
);
|
||||
|
||||
const result: UseAsyncDataReturn<ModelsListResponseBody[]> = useAsyncData<ModelsListResponseBody[]>(
|
||||
() => ModelAPI.fetchModels({page: 1, page_size: 200}),
|
||||
async () => {
|
||||
const models = await ModelAPI.fetchModels({page: 1, page_size: 200});
|
||||
// 异步写入加密缓存(best-effort,不影响数据流)
|
||||
setCachedModels(models).catch(() => {});
|
||||
modelFirstFetchDone = true;
|
||||
setInstantData(models);
|
||||
return models;
|
||||
},
|
||||
[],
|
||||
{enabled: isLoggedIn, label: 'model-list'},
|
||||
);
|
||||
|
||||
// 合并策略:API 数据 > 缓存即时数据
|
||||
const mergedData = result.data ?? instantData;
|
||||
|
||||
// loading 仅在无任何数据时展示(缓存命中时不闪烁 loading)
|
||||
const loading = result.loading && mergedData === null;
|
||||
|
||||
// 按 category_name 分组(后端返回的 category_name 为 slug 格式如 img2img)
|
||||
const groupedModels: Map<string, ModelsListResponseBody[]> = useMemo(() => {
|
||||
const groups = new Map<string, ModelsListResponseBody[]>();
|
||||
result.data?.forEach((model: ModelsListResponseBody) => {
|
||||
mergedData?.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]);
|
||||
}, [mergedData]);
|
||||
|
||||
return {
|
||||
...result,
|
||||
/** 合并后的模型数据(缓存优先) */
|
||||
data: mergedData,
|
||||
/** 仅在无缓存且正在加载时为 true */
|
||||
loading,
|
||||
/** 按 model_type 分组后的模型(key 为后端 slug) */
|
||||
groupedModels,
|
||||
};
|
||||
@@ -121,12 +148,12 @@ export function useTaskDetail(taskId: string | null | undefined) {
|
||||
// 提交任务
|
||||
// ============================================================
|
||||
|
||||
/** 提交任务 Mutation Hook */
|
||||
/** 提交任务 Mutation Hook(device_id 由 API 层自动注入,调用方无需传入) */
|
||||
export function useSubmitTask(options?: {
|
||||
onSuccess?: (data: TaskInfoItemResponseBody) => void;
|
||||
onError?: (err: Error) => void;
|
||||
}) {
|
||||
return useAsyncMutation<TaskInfoItemResponseBody, [CreatTaskRequestBody]>(
|
||||
return useAsyncMutation<TaskInfoItemResponseBody, [Omit<CreatTaskRequestBody, 'device_id'>]>(
|
||||
(body) => TaskAPI.submitTask(body),
|
||||
{
|
||||
label: 'submit-task',
|
||||
|
||||
Reference in New Issue
Block a user