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,99 @@
import { useState } from "react";
import { useNavigate, Navigate } from "react-router-dom";
import { QrCode, Loader2 } from "lucide-react";
import { useAuth } from "../../contexts/AuthContext";
export default function AdminLoginPage() {
const { login, isAuthenticated } = useAuth();
const navigate = useNavigate();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 已登录 → 直接跳转
if (isAuthenticated) {
return <Navigate to="/admin/dashboard" replace />;
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!username.trim() || !password.trim()) return;
setLoading(true);
setError(null);
try {
await login(username.trim(), password);
navigate("/admin/dashboard", { replace: true });
} catch (err: any) {
setError(err?.message ?? "登录失败,请检查用户名和密码");
} finally {
setLoading(false);
}
}
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50">
<div className="w-full max-w-sm rounded-xl bg-white p-8 shadow-lg">
{/* Logo */}
<div className="mb-6 flex flex-col items-center">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-600">
<QrCode className="h-6 w-6 text-white" />
</div>
<h1 className="mt-3 text-lg font-bold text-gray-800">
生产流转 · 管理端
</h1>
<p className="mt-1 text-sm text-gray-500">请使用工号登录</p>
</div>
{/* 错误 */}
{error && (
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
{error}
</div>
)}
{/* 表单 */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-gray-600">
用户名
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="如 IRIS 或工号"
className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-100"
autoFocus
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-600">
密码
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="输入密码"
className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-100"
/>
</div>
<button
type="submit"
disabled={loading || !username.trim() || !password.trim()}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
>
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
登录
</button>
</form>
</div>
</div>
);
}