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:
@@ -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 });
|
||||
}
|
||||
|
||||
/** 修改密码(已登录) */
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
57
src/services/modules/billing.ts
Normal file
57
src/services/modules/billing.ts
Normal 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;
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
/** 轻量轮询任务状态 */
|
||||
|
||||
@@ -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 套餐购买订单 */
|
||||
|
||||
Reference in New Issue
Block a user