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

@@ -32,7 +32,7 @@ export const UserRoleTypeLabelMap: Record<UserRoleTypeValue, string> = {
[UserRoleTypeEnum.User]: "用户账户",
[UserRoleTypeEnum.Admin]: "管理账户",
[UserRoleTypeEnum.SuperAdmin]: "超管账户"
}
};
export const UserAccountTypeEnum = {
Individual: 'individual',
@@ -197,8 +197,8 @@ export class AuthAPI {
}
/** 获取当前用户信息 */
static async getUserInfo(): Promise<UserInfoBody> {
return await get<UserInfoBody>(AuthUrlObj.userInfo);
static async getUserInfo(signal?: AbortSignal): Promise<UserInfoBody> {
return await get<UserInfoBody>(AuthUrlObj.userInfo, undefined, { signal });
}
/** 修改密码(已登录) */

View File

@@ -38,6 +38,6 @@ export type BannerListResponse = Banner[];
* 获取 Banner 轮播图列表
* GET /api/v1/banners
*/
export function fetchBanners(): Promise<BannerListResponse> {
return get<BannerListResponse>(BannerUrlObj.list);
export function fetchBanners(signal?: AbortSignal): Promise<BannerListResponse> {
return get<BannerListResponse>(BannerUrlObj.list, undefined, { signal });
}

View File

@@ -0,0 +1,57 @@
import {type Nullable} from "@/utils/type";
import {get} from "@/services/request.ts";
const BillingUrlObj = {
getBalance: '/api/v1/billing/balance',
getLedger: '/api/v1/billing/ledger',
ExportLedger: "/api/v1/billing/ledger/export"
} as const;
export interface BalanceResponseBody {
user_id: string,
balance_cent: Nullable<number>,
gift_balance_cent: Nullable<number>,
currency: "CNY",
today_spent_cent: number,
today_calls: number
}
export interface LedgerReqeustParam {
month: Nullable<Date>,
page: number,
page_size: number
}
export interface LedgerItemResponseBody {
id: string,
entry_type: string,
balance_cent: string,
amount_cent: number,
currency: string,
balance_after_cent: number,
task_id: string,
description: "string",
accounting_month: "string",
accounting_day: "string",
created_at: Date
}
export interface ExportLedgerRequstParam {
start_time: Date,
end_time: Date,
entry_type: Nullable<string>
debug?: boolean
}
export class BillingAPI {
static getBalance(signal?: AbortSignal): Promise<BalanceResponseBody> {
return get<BalanceResponseBody>(BillingUrlObj.getBalance, undefined, {signal});
}
static getLedger(param: LedgerReqeustParam, signal?: AbortSignal): Promise<LedgerItemResponseBody[]> {
return get<LedgerItemResponseBody[]>(BillingUrlObj.getLedger, param as unknown as Record<string, unknown>, {signal});
}
static exportUrl = BillingUrlObj.ExportLedger;
}

View File

@@ -144,8 +144,8 @@ 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>);
static fetchModels(params: ModelsListReqeustParams, signal?: AbortSignal): Promise<ModelsListResponseBody[]> {
return get<ModelsListResponseBody[]>(ModelsUrlObj.modelList, params as unknown as Record<string, unknown>, { signal });
}
/**

View File

@@ -11,7 +11,7 @@
// ============================================================
import {get, post} from '../request';
import {getDeviceId} from '../../utils/device';
import {getDeviceId} from '@/utils/device.ts';
// ============================================================
// 基础枚举 / 字面量类型
@@ -207,8 +207,8 @@ const TaskUrlObj = {
export class TaskAPI {
/** 获取任务列表(分页/游标) */
static getTaskList(params: TaskListRequestParams): Promise<TaskInfoDataResponseBody> {
return get<TaskInfoDataResponseBody>(TaskUrlObj.task, params as Record<string, unknown>);
static getTaskList(params: TaskListRequestParams, signal?: AbortSignal): Promise<TaskInfoDataResponseBody> {
return get<TaskInfoDataResponseBody>(TaskUrlObj.task, params as Record<string, unknown>, { signal });
}
/** 提交新任务device_id 由 API 层自动注入 getDeviceId(),调用方无需传入) */
@@ -220,8 +220,8 @@ export class TaskAPI {
}
/** 获取任务详情 */
static getTaskInfo(task_id: string): Promise<TaskInfoItemResponseBody> {
return get<TaskInfoItemResponseBody>(TaskUrlObj.taskInfo(task_id));
static getTaskInfo(task_id: string, signal?: AbortSignal): Promise<TaskInfoItemResponseBody> {
return get<TaskInfoItemResponseBody>(TaskUrlObj.taskInfo(task_id), undefined, { signal });
}
/** 轻量轮询任务状态 */

View File

@@ -1,7 +1,7 @@
import {post, get} from '../request';
import {type UserInfoBody} from "@/services/modules/auth";
import {type Nullable} from "@/utils/type";
type Nullable<T> = T | null;
// ============================================================
// VipApiUrl — VIP 模块路由常量
@@ -133,8 +133,8 @@ export class VipAPI {
}
/** 获取 VIP 等级列表(已登录时按账户类型过滤) */
static async getVipLevels(): Promise<VipLevelsItem[]> {
return await get<VipLevelsItem[]>(VipApiUrl.GetVipLevelsList);
static async getVipLevels(signal?: AbortSignal): Promise<VipLevelsItem[]> {
return await get<VipLevelsItem[]>(VipApiUrl.GetVipLevelsList, undefined, {signal});
}
/** 创建 VIP 套餐购买订单 */