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:
@@ -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' },
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 提交任务
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user