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

@ -0,0 +1,122 @@
import {
createContext,
useContext,
useState,
useCallback,
useEffect,
type ReactNode,
} from "react";
import { login as loginApi, getMe } from "../services/authApi";
// ============================================================
// 类型
// ============================================================
export interface UserInfo {
id: string;
username: string;
display_name: string;
role: string;
}
interface AuthState {
user: UserInfo | null;
token: string | null;
loading: boolean; // 初始化时检查 token
}
interface AuthContextValue extends AuthState {
login: (username: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}
// ============================================================
// Token 存储 key
// ============================================================
const TOKEN_KEY = "track_admin_token";
const USER_KEY = "track_admin_user";
// ============================================================
// Context
// ============================================================
const AuthContext = createContext<AuthContextValue | null>(null);
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth 必须在 AuthProvider 内使用");
return ctx;
}
// ============================================================
// Provider
// ============================================================
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<AuthState>({
user: null,
token: null,
loading: true,
});
// 初始化:从 localStorage 恢复 token
useEffect(() => {
const savedToken = localStorage.getItem(TOKEN_KEY);
const savedUser = localStorage.getItem(USER_KEY);
if (savedToken && savedUser) {
try {
const user = JSON.parse(savedUser) as UserInfo;
setState({ user, token: savedToken, loading: false });
// 可选:后端验证 token 是否仍有效
getMe(savedToken)
.then((fresh) => {
setState((prev) => ({ ...prev, user: fresh }));
localStorage.setItem(USER_KEY, JSON.stringify(fresh));
})
.catch(() => {
// token 过期,清除
logoutInternal();
});
} catch {
logoutInternal();
}
} else {
setState((prev) => ({ ...prev, loading: false }));
}
}, []);
function logoutInternal() {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
setState({ user: null, token: null, loading: false });
}
const login = useCallback(async (username: string, password: string) => {
const result = await loginApi(username, password);
const token = result.access_token;
const user: UserInfo = result.user;
localStorage.setItem(TOKEN_KEY, token);
localStorage.setItem(USER_KEY, JSON.stringify(user));
setState({ user, token, loading: false });
}, []);
const logout = useCallback(() => {
logoutInternal();
}, []);
return (
<AuthContext.Provider
value={{
...state,
login,
logout,
isAuthenticated: !!state.token && !!state.user,
}}
>
{children}
</AuthContext.Provider>
);
}