## 架构重构
- 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 不安全)
94 lines
2.6 KiB
TypeScript
94 lines
2.6 KiB
TypeScript
// ============================================================
|
||
// API 响应通用类型
|
||
// ============================================================
|
||
|
||
/** 后端统一响应结构 */
|
||
export interface ApiResponse<T = unknown> {
|
||
/** 业务状态码:0 表示成功 */
|
||
code: number;
|
||
/** 响应数据 */
|
||
data: T;
|
||
/** 提示信息 */
|
||
message: string;
|
||
/** 请求追踪 ID(可选,用于排查问题) */
|
||
traceId?: string;
|
||
}
|
||
|
||
/** 分页请求参数 */
|
||
export interface PaginationParams {
|
||
page: number;
|
||
pageSize: number;
|
||
}
|
||
|
||
/** 分页响应数据 */
|
||
export interface PaginatedData<T> {
|
||
/** 数据列表 */
|
||
list: T[];
|
||
/** 总条数 */
|
||
total: number;
|
||
/** 当前页码 */
|
||
page: number;
|
||
/** 每页条数 */
|
||
pageSize: number;
|
||
}
|
||
|
||
/** 分页响应包装 */
|
||
export type PaginatedResponse<T> = ApiResponse<PaginatedData<T>>;
|
||
|
||
/** 请求错误类型 */
|
||
export enum RequestErrorType {
|
||
/** 网络错误(断网等) */
|
||
NETWORK = 'NETWORK',
|
||
/** 请求超时 */
|
||
TIMEOUT = 'TIMEOUT',
|
||
/** HTTP 状态码异常(4xx / 5xx) */
|
||
HTTP = 'HTTP',
|
||
/** 业务错误(code ≠ 0) */
|
||
BUSINESS = 'BUSINESS',
|
||
/** 请求被取消 */
|
||
CANCELLED = 'CANCELLED',
|
||
/** 认证过期(refresh_token 失效,需重新登录) */
|
||
AUTH_EXPIRED = 'AUTH_EXPIRED',
|
||
}
|
||
|
||
/** 请求错误 */
|
||
export class RequestError extends Error {
|
||
type: RequestErrorType;
|
||
code?: number;
|
||
httpStatus?: number;
|
||
traceId?: string;
|
||
|
||
constructor(
|
||
type: RequestErrorType,
|
||
message: string,
|
||
options?: { code?: number; httpStatus?: number; traceId?: string },
|
||
) {
|
||
super(message);
|
||
this.name = 'RequestError';
|
||
this.type = type;
|
||
this.code = options?.code;
|
||
this.httpStatus = options?.httpStatus;
|
||
this.traceId = options?.traceId;
|
||
}
|
||
}
|
||
|
||
// ---------- 自定义错误子类(全局统一捕获 → 事件总线触发)----------
|
||
|
||
/**
|
||
* RefreshError — refresh_token 过期/无效,需重新登录。
|
||
* 全局响应拦截器自动捕获此错误并 emit AUTH_REQUIRED,调用方无需手动处理。
|
||
*
|
||
* 用法:throw new RefreshError('登录已过期,请重新登录');
|
||
*/
|
||
export class RefreshError extends RequestError {
|
||
constructor(message = '登录已过期,请重新登录') {
|
||
super(RequestErrorType.AUTH_EXPIRED, message);
|
||
this.name = 'RefreshError';
|
||
}
|
||
}
|
||
|
||
/** 判断一个错误是否为 RefreshError(支持 instanceof 或 type 判断) */
|
||
export function isRefreshError(err: unknown): err is RefreshError {
|
||
return err instanceof RefreshError || (err as RequestError)?.type === RequestErrorType.AUTH_EXPIRED;
|
||
}
|