feat: 数据获取架构重构 + Mac指纹采集 + Token刷新容错 + 账单页面 (0.0.19)

## 架构重构
  - useAsyncData 集成 AbortController:cleanup 时真正中断 HTTP 请求,
    不再仅丢弃响应(解决 StrictMode 双重请求问题)
  - API 模块统一 signal?: AbortSignal 参数,透传 axios config
  - use-api.ts 成为数据 Hook 集中管理中心,新增 useAccountInfo、
    useBillingBalance、useBillingLedger
  - 页面组件改为纯渲染:const {data,loading,error,refetch} = useXxx()

  ## 新功能
  - Mac 硬件指纹采集(electron/preload/device.ts):
    ioreg(UUID) + sysctl(CPU) + system_profiler(HDD三级回退)
  - 账单页面 BillingInfo(余额卡片 + 账单表格 + 分页)
  - PageDispatcher 注册 /billing 页面(FloatingPanel 960×680)
  - 共享工具函数 centToYuan / formatDate(src/utils/display.ts)

  ## Bug 修复
  - Token 刷新容错:网络错误时保留 refresh_token,等网络恢复后重试;
    仅 RefreshError(token 过期)时清除(auth-token.ts)
  - AppProvider 自动登录时 nullable 字段默认值(email→''、admin_permissions→[])
  - AccountInfo 重构消除 ~40 行手写状态管理代码

  ## 代码质量
  - AccountInfo 消除 centToYuan/formatDate 重复定义
  - BillingInfo 标题层级精简(移除冗余页面/Card标题)
  - antd Table align 字面量类型修正('left' as const)
  - billing.ts signal 参数位置修复(params→config)
  - modelFirstFetchDone 模块级变量移除(StrictMode 不安全)
This commit is contained in:
2026-06-11 20:08:44 +08:00
parent 95019316b2
commit 4237dcbaf6
24 changed files with 808 additions and 172 deletions

View File

@@ -17,6 +17,9 @@ 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 { AuthAPI, type UserInfoBody } from '@/services/modules/auth';
import { VipAPI, type VipLevelsItem } from '@/services/modules/vip-api';
import { BillingAPI, type BalanceResponseBody, type LedgerItemResponseBody, type LedgerReqeustParam } from '@/services/modules/billing';
import {useAsyncData, UseAsyncDataReturn, useAsyncMutation} from './use-async';
import {getCachedModels, setCachedModels} from '@/services/cache/model-cache';
@@ -29,7 +32,7 @@ export function useBannerList() {
const {isLoggedIn} = useAppContext();
return useAsyncData<Banner[]>(
fetchBanners,
(signal) => fetchBanners(signal),
[],
{enabled: isLoggedIn, label: 'banner-list'},
);
@@ -39,9 +42,6 @@ export function useBannerList() {
// 模型
// ============================================================
/** 模块级:单次会话中是否已完成首次 API 拉取 */
let modelFirstFetchDone = false;
/** 模型列表数据 Hook缓存优先 + 后台刷新)
*
* 策略:
@@ -61,11 +61,10 @@ export function useModelList() {
);
const result: UseAsyncDataReturn<ModelsListResponseBody[]> = useAsyncData<ModelsListResponseBody[]>(
async () => {
const models = await ModelAPI.fetchModels({page: 1, page_size: 200});
async (signal) => {
const models = await ModelAPI.fetchModels({page: 1, page_size: 200}, signal);
// 异步写入加密缓存best-effort不影响数据流
setCachedModels(models).catch(() => {});
modelFirstFetchDone = true;
setInstantData(models);
return models;
},
@@ -123,7 +122,7 @@ export function useTaskList(params: UseTaskListParams) {
const {requestParams, refreshSignal = 0} = params;
return useAsyncData(
() => TaskAPI.getTaskList(requestParams),
(signal) => TaskAPI.getTaskList(requestParams, signal),
[requestParams, refreshSignal],
{enabled: isLoggedIn, label: 'task-list'},
);
@@ -138,12 +137,53 @@ export function useTaskDetail(taskId: string | null | undefined) {
const {isLoggedIn} = useAppContext();
return useAsyncData<TaskInfoItemResponseBody>(
() => TaskAPI.getTaskInfo(taskId!),
(signal) => TaskAPI.getTaskInfo(taskId!, signal),
[taskId],
{enabled: isLoggedIn && !!taskId, label: 'task-detail'},
);
}
// ============================================================
// 账户信息(用户信息 + VIP 等级,并行请求)
// ============================================================
/** 账户信息页数据 Hook — 返回 [userInfo, vipsInfo] 元组 */
export function useAccountInfo() {
const { isLoggedIn } = useAppContext();
return useAsyncData<[UserInfoBody, VipLevelsItem[]]>(
(signal) => Promise.all([
AuthAPI.getUserInfo(signal),
VipAPI.getVipLevels(signal),
]),
[],
{ enabled: isLoggedIn, label: 'account-info' },
);
}
// ============================================================
// 账单
// ============================================================
/** 账户余额 Hook */
export function useBillingBalance() {
const { isLoggedIn } = useAppContext();
return useAsyncData<BalanceResponseBody>(
(signal) => BillingAPI.getBalance(signal),
[],
{ enabled: isLoggedIn, label: 'billing-balance' },
);
}
/** 账单明细 Hook支持分页参数 */
export function useBillingLedger(params: LedgerReqeustParam) {
const { isLoggedIn } = useAppContext();
return useAsyncData<LedgerItemResponseBody[]>(
(signal) => BillingAPI.getLedger(params, signal),
[params],
{ enabled: isLoggedIn, label: 'billing-ledger' },
);
}
// ============================================================
// 提交任务
// ============================================================