Artifact 4071cf65fa97847e7569216a7d5abbfa79f7886af476ed3584f608c9638745e7:
- File
web/src/api.ts
— part of check-in
[dfa747c1ad]
at
2026-10-23 06:08:28
on branch trunk
— Web 管理控制台
- 用 Vite 和 React 做完整的 Web 端:登录、按权限显示菜单、切换当前作业仓库 - 覆盖来肯同步、仓库、库位规划、商品、销售订单、采购入库与批次码、采购订单、库存、用户授权和审计日志 - 开发时由 Vite 把 /api 代理到本机后端 - SQLite 写入和页面读取冲突时自动重试,避免请求进程因数据库忙碌直接崩溃 (user: pluto size: 1856)
export const TOKEN_KEY = 'swiftflow.token' export class ApiError extends Error { status: number constructor(status: number, message: string) { super(message) this.status = status } } export type SessionUser = { id: string username: string display_name: string roles: string[] permissions: string[] all_warehouses: boolean warehouse_ids: number[] current_warehouse_id: number terminal: string expires_at: string } export type Warehouse = { id: number code: string name: string easycode: string operational: boolean } export function can(user: SessionUser | null, permission: string) { return user?.permissions.includes(permission) ?? false } export async function api<T>(path: string, init: RequestInit = {}): Promise<T> { const headers = new Headers(init.headers) if (init.body && !headers.has('content-type')) { headers.set('content-type', 'application/json') } const token = localStorage.getItem(TOKEN_KEY) if (token) headers.set('authorization', `Bearer ${token}`) const response = await fetch(path, { ...init, headers }) const text = await response.text() let data: { error?: string } = {} if (text) { try { data = JSON.parse(text) as { error?: string } } catch { data = {} } } if (response.status === 401 && !path.endsWith('/api/auth/login')) { localStorage.removeItem(TOKEN_KEY) window.dispatchEvent(new Event('swiftflow-unauthorized')) } if (!response.ok) { throw new ApiError(response.status, data.error || `请求失败(${response.status})`) } return data as T } export function qty(value: number) { return value.toLocaleString('zh-CN', { maximumFractionDigits: 4 }) } export function money(value: number) { return value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2, }) }