feat(frontend): 认证体系 + 类型定义 + API 服务层

认证:
- AuthContext + useAuth (JWT token 持久化, 登录/登出)
- AdminLoginPage (登录表单, 对接 POST /auth/login)
- axios 拦截器自动注入 Authorization header
- 401 自动清除 token 并跳转登录页

类型:
- api.ts: TASK_STATUS 常量, TaskResponse/ProductScanResponse 扩展字段
- admin.ts: ProductResponse 扩展 + MaterialOption/BomItem

API 服务:
- authApi: login() + getMe()
- materialApi: fetchMaterialGroups() + fetchMaterialItems()
- printApi: getLabelPreview() + executePrint() + getPrinterConfig()
- antd 6.5.3 依赖

字体: simhei.ttf (标签打印用)
This commit is contained in:
2026-08-05 14:01:08 +08:00
parent 1fed716829
commit d04085fc38
10 changed files with 1352 additions and 7 deletions

View File

@ -8,11 +8,13 @@ const api = axios.create({
},
});
// 请求拦截器 — 可在此注入 token
// 请求拦截器 — 注入 JWT token
api.interceptors.request.use(
(config) => {
// const token = localStorage.getItem("access_token");
// if (token) config.headers.Authorization = `Bearer ${token}`;
const token = localStorage.getItem("track_admin_token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error),
@ -23,7 +25,12 @@ api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// 未授权:跳转登录 / 刷新 token
// Token 过期或无效 → 清除并跳转登录
localStorage.removeItem("track_admin_token");
localStorage.removeItem("track_admin_user");
if (window.location.pathname.startsWith("/admin")) {
window.location.href = "/admin/login";
}
}
return Promise.reject(error);
},

View File

@ -0,0 +1,34 @@
import axios from "axios";
import type { UserInfo } from "../contexts/AuthContext";
const API_BASE = import.meta.env.VITE_API_BASE_URL;
export interface LoginResponse {
access_token: string;
user: UserInfo;
}
/** 登录 — 注意:此请求不走 axios 实例(避免循环依赖),直接用 fetch */
export async function login(username: string, password: string): Promise<LoginResponse> {
const res = await fetch(`${API_BASE}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error((err as any).detail ?? "登录失败");
}
return res.json();
}
/** 验证 token 有效性 + 获取最新用户信息 */
export async function getMe(token: string): Promise<UserInfo> {
const res = await fetch(`${API_BASE}/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
throw new Error("Token 无效");
}
return res.json();
}

View File

@ -0,0 +1,43 @@
import api from "./api";
// ============================================================
// 类型 — 对标 MOM material_base
// ============================================================
export interface MaterialGroup {
category: string;
count: number;
}
export interface MaterialItem {
id: number;
name: string; // material_base.name
spec: string; // material_base.spec_model
category: string; // material_base.category
type: string; // material_base.material_type
unit: string; // material_base.unit
is_enabled: boolean;
}
// ============================================================
// API
// ============================================================
/** 获取物料分组摘要 — 手风琴外层 */
export async function fetchMaterialGroups(keyword?: string): Promise<MaterialGroup[]> {
const { data } = await api.get<MaterialGroup[]>("/materials/groups", {
params: keyword ? { keyword } : {},
});
return data;
}
/** 获取指定分类下的物料条目 — 手风琴展开时懒加载 */
export async function fetchMaterialItems(
category: string,
keyword?: string
): Promise<MaterialItem[]> {
const { data } = await api.get<MaterialItem[]>("/materials/items", {
params: { category, keyword: keyword || "", limit: 9999 },
});
return data;
}

View File

@ -0,0 +1,72 @@
import api from "./api";
// ============================================================
// 类型
// ============================================================
export interface LabelPreviewRequest {
serial_number: string;
material_name?: string;
spec_model?: string;
order_no?: string;
}
export interface PrintExecuteRequest extends LabelPreviewRequest {
copies: number;
printer_ip?: string;
printer_port?: number;
}
export interface PrinterConfig {
label_printer: {
ip: string;
port: number;
enabled: boolean;
};
}
// ============================================================
// API
// ============================================================
/** 生成标签预览图 → Base64 data URL */
export async function getLabelPreview(
data: LabelPreviewRequest
): Promise<string> {
const { data: res } = await api.post<{ data_url: string }>(
"/print/preview",
data
);
return res.data_url;
}
/** 发送打印指令到打标机 */
export async function executePrint(
data: PrintExecuteRequest
): Promise<{ message: string }> {
const { data: res } = await api.post<{ message: string }>(
"/print/execute",
data
);
return res;
}
/** 获取打印机配置 */
export async function getPrinterConfig(): Promise<PrinterConfig> {
const { data } = await api.get<PrinterConfig>("/print/config");
return data;
}
/** 更新打印机配置 */
export async function updatePrinterConfig(
ip: string,
port: number,
enabled: boolean = true
): Promise<{ message: string }> {
const { data } = await api.post<{ message: string }>("/print/config", {
ip,
port,
enabled,
});
return data;
}