Compare commits
4 Commits
master
...
dc97a7385f
| Author | SHA1 | Date | |
|---|---|---|---|
| dc97a7385f | |||
| b725599cb0 | |||
| 3d164159ac | |||
| 9d204a57d8 |
@ -1,20 +1,26 @@
|
|||||||
|
import { Suspense, lazy } from "react";
|
||||||
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
||||||
import { App as AntApp } from "antd";
|
import { App as AntApp } from "antd";
|
||||||
|
|
||||||
import { ToastProvider } from "./components/ui/Toast";
|
import { ToastProvider } from "./components/ui/Toast";
|
||||||
import { AuthProvider } from "./contexts/AuthContext";
|
import { AuthProvider } from "./contexts/AuthContext";
|
||||||
import AppLayout from "./components/layout/AppLayout";
|
import LoadingSpinner from "./components/ui/LoadingSpinner";
|
||||||
import ScanPage from "./pages/ScanPage";
|
|
||||||
import MyTasksPage from "./pages/MyTasksPage";
|
|
||||||
import NotificationsPage from "./pages/NotificationsPage";
|
|
||||||
import ProfilePage from "./pages/ProfilePage";
|
|
||||||
|
|
||||||
|
// Layout 组件静态导入(始终需要,体积小)
|
||||||
|
import AppLayout from "./components/layout/AppLayout";
|
||||||
import AdminLayout from "./components/layout/AdminLayout";
|
import AdminLayout from "./components/layout/AdminLayout";
|
||||||
import AdminLoginPage from "./pages/admin/AdminLoginPage";
|
|
||||||
import AdminDashboard from "./pages/admin/AdminDashboard";
|
// 🚀 路由级代码分割 — 按需懒加载页面组件
|
||||||
import AdminProductsPage from "./pages/admin/AdminProductsPage";
|
const ScanPage = lazy(() => import("./pages/ScanPage"));
|
||||||
import AdminTasksPage from "./pages/admin/AdminTasksPage";
|
const MyTasksPage = lazy(() => import("./pages/MyTasksPage"));
|
||||||
import AdminPrintConfigPage from "./pages/admin/AdminPrintConfigPage";
|
const NotificationsPage = lazy(() => import("./pages/NotificationsPage"));
|
||||||
|
const ProfilePage = lazy(() => import("./pages/ProfilePage"));
|
||||||
|
|
||||||
|
const AdminLoginPage = lazy(() => import("./pages/admin/AdminLoginPage"));
|
||||||
|
const AdminDashboard = lazy(() => import("./pages/admin/AdminDashboard"));
|
||||||
|
const AdminProductsPage = lazy(() => import("./pages/admin/AdminProductsPage"));
|
||||||
|
const AdminTasksPage = lazy(() => import("./pages/admin/AdminTasksPage"));
|
||||||
|
const AdminPrintConfigPage = lazy(() => import("./pages/admin/AdminPrintConfigPage"));
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@ -22,28 +28,30 @@ export default function App() {
|
|||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
{/* 移动端 */}
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to="/scan" replace />} />
|
{/* 移动端 */}
|
||||||
<Route element={<AppLayout />}>
|
<Route path="/" element={<Navigate to="/admin/dashboard" replace />} />
|
||||||
<Route path="/scan" element={<ScanPage />} />
|
<Route element={<AppLayout />}>
|
||||||
<Route path="/tasks" element={<MyTasksPage />} />
|
<Route path="/scan" element={<ScanPage />} />
|
||||||
<Route path="/notifications" element={<NotificationsPage />} />
|
<Route path="/tasks" element={<MyTasksPage />} />
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
<Route path="/notifications" element={<NotificationsPage />} />
|
||||||
</Route>
|
<Route path="/profile" element={<ProfilePage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
{/* PC 管理端 — 登录页(独立,无侧边栏) */}
|
{/* PC 管理端 — 登录页(独立,无侧边栏) */}
|
||||||
<Route path="/admin/login" element={<AdminLoginPage />} />
|
<Route path="/admin/login" element={<AdminLoginPage />} />
|
||||||
|
|
||||||
{/* PC 管理端 — 需要登录 */}
|
{/* PC 管理端 — 需要登录 */}
|
||||||
<Route path="/admin" element={<Navigate to="/admin/dashboard" replace />} />
|
<Route path="/admin" element={<Navigate to="/admin/dashboard" replace />} />
|
||||||
<Route element={<AdminLayout />}>
|
<Route element={<AdminLayout />}>
|
||||||
<Route path="/admin/dashboard" element={<AdminDashboard />} />
|
<Route path="/admin/dashboard" element={<AdminDashboard />} />
|
||||||
<Route path="/admin/products" element={<AdminProductsPage />} />
|
<Route path="/admin/products" element={<AdminProductsPage />} />
|
||||||
<Route path="/admin/tasks" element={<AdminTasksPage />} />
|
<Route path="/admin/tasks" element={<AdminTasksPage />} />
|
||||||
<Route path="/admin/print-config" element={<AdminPrintConfigPage />} />
|
<Route path="/admin/print-config" element={<AdminPrintConfigPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback, useMemo, memo } from "react";
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Loader2,
|
Loader2,
|
||||||
@ -75,7 +75,7 @@ function getStatusConfig(status: string) {
|
|||||||
// 通用 Modal 容器
|
// 通用 Modal 容器
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function Modal({
|
const Modal = memo(function Modal({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
title,
|
title,
|
||||||
@ -110,13 +110,13 @@ function Modal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 确认接收弹窗
|
// 确认接收弹窗
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function ReceiveConfirmModal({
|
const ReceiveConfirmModal = memo(function ReceiveConfirmModal({
|
||||||
open,
|
open,
|
||||||
task,
|
task,
|
||||||
submitting,
|
submitting,
|
||||||
@ -163,13 +163,13 @@ function ReceiveConfirmModal({
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 品质驳回弹窗
|
// 品质驳回弹窗
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function RejectModal({
|
const RejectModal = memo(function RejectModal({
|
||||||
open,
|
open,
|
||||||
task,
|
task,
|
||||||
submitting,
|
submitting,
|
||||||
@ -244,13 +244,13 @@ function RejectModal({
|
|||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 完工裂变转交弹窗
|
// 完工裂变转交弹窗
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function TransferModal({
|
const TransferModal = memo(function TransferModal({
|
||||||
open,
|
open,
|
||||||
task,
|
task,
|
||||||
submitting,
|
submitting,
|
||||||
@ -496,7 +496,7 @@ function TransferModal({
|
|||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 单个任务节点卡片
|
// 单个任务节点卡片
|
||||||
@ -507,7 +507,7 @@ interface ModalTarget {
|
|||||||
action: "receive" | "reject" | "transfer";
|
action: "receive" | "reject" | "transfer";
|
||||||
}
|
}
|
||||||
|
|
||||||
function TaskNodeCard({
|
const TaskNodeCard = memo(function TaskNodeCard({
|
||||||
task,
|
task,
|
||||||
isLast,
|
isLast,
|
||||||
onAction,
|
onAction,
|
||||||
@ -653,7 +653,7 @@ function TaskNodeCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 主容器组件
|
// 主容器组件
|
||||||
@ -779,6 +779,12 @@ export default function TaskTreeViewer() {
|
|||||||
|
|
||||||
// ---- 渲染 ----
|
// ---- 渲染 ----
|
||||||
|
|
||||||
|
// 🚀 缓存递归计算 — 仅在 task_tree 变化时重新计算
|
||||||
|
const totalTaskCount = useMemo(
|
||||||
|
() => (product?.task_tree ? countAllTasks(product.task_tree) : 0),
|
||||||
|
[product?.task_tree],
|
||||||
|
);
|
||||||
|
|
||||||
const modalTask = modalTarget?.task ?? null;
|
const modalTask = modalTarget?.task ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -880,7 +886,7 @@ export default function TaskTreeViewer() {
|
|||||||
<p className="text-sm font-semibold text-gray-800">
|
<p className="text-sm font-semibold text-gray-800">
|
||||||
{product.top_level_tasks?.length ?? 0} 顶层
|
{product.top_level_tasks?.length ?? 0} 顶层
|
||||||
{product.task_tree?.length
|
{product.task_tree?.length
|
||||||
? ` · ${countAllTasks(product.task_tree)} 总计`
|
? ` · ${totalTaskCount} 总计`
|
||||||
: ""}
|
: ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
/** 任务进度列表卡片 — 递归渲染 task_tree */
|
/** 任务进度列表卡片 — 递归渲染 task_tree */
|
||||||
|
import { useMemo, memo } from "react";
|
||||||
import { ClipboardList, GitBranch, AlertTriangle } from "lucide-react";
|
import { ClipboardList, GitBranch, AlertTriangle } from "lucide-react";
|
||||||
import type { TaskResponse, TaskSummary } from "../../types/api";
|
import type { TaskResponse, TaskSummary } from "../../types/api";
|
||||||
import { TASK_STATUS } from "../../types/api";
|
import { TASK_STATUS } from "../../types/api";
|
||||||
@ -30,7 +31,7 @@ interface TaskNodeProps {
|
|||||||
depth: number;
|
depth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TaskNode({ task, depth }: TaskNodeProps) {
|
const TaskNode = memo(function TaskNode({ task, depth }: TaskNodeProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-3 px-4 py-3 border-t border-gray-50" style={{ paddingLeft: 16 + depth * 16 }}>
|
<div className="flex items-center gap-3 px-4 py-3 border-t border-gray-50" style={{ paddingLeft: 16 + depth * 16 }}>
|
||||||
@ -62,19 +63,22 @@ function TaskNode({ task, depth }: TaskNodeProps) {
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
function countAll(tasks: TaskResponse[]): number {
|
function countAll(tasks: TaskResponse[]): number {
|
||||||
return tasks.reduce((s, t) => s + 1 + (t.child_tasks ? countAll(t.child_tasks) : 0), 0);
|
return tasks.reduce((s, t) => s + 1 + (t.child_tasks ? countAll(t.child_tasks) : 0), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TaskListCard({ tasks }: TaskListCardProps) {
|
export default function TaskListCard({ tasks }: TaskListCardProps) {
|
||||||
|
// 🚀 缓存递归计算
|
||||||
|
const totalCount = useMemo(() => countAll(tasks), [tasks]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl bg-white shadow-sm">
|
<div className="rounded-xl bg-white shadow-sm">
|
||||||
<div className="flex items-center gap-2 border-b border-gray-100 px-4 py-3">
|
<div className="flex items-center gap-2 border-b border-gray-100 px-4 py-3">
|
||||||
<ClipboardList className="h-5 w-5 text-blue-600" />
|
<ClipboardList className="h-5 w-5 text-blue-600" />
|
||||||
<h3 className="font-semibold text-gray-800">任务流转树</h3>
|
<h3 className="font-semibold text-gray-800">任务流转树</h3>
|
||||||
<span className="ml-auto text-xs text-gray-400">{countAll(tasks)} 个任务</span>
|
<span className="ml-auto text-xs text-gray-400">{totalCount} 个任务</span>
|
||||||
</div>
|
</div>
|
||||||
{tasks.length === 0 ? (
|
{tasks.length === 0 ? (
|
||||||
<div className="px-4 py-8 text-center text-sm text-gray-400">暂无关联任务</div>
|
<div className="px-4 py-8 text-center text-sm text-gray-400">暂无关联任务</div>
|
||||||
|
|||||||
10
frontend/src/components/ui/LoadingSpinner.tsx
Normal file
10
frontend/src/components/ui/LoadingSpinner.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
|
/** 全局路由懒加载回退组件 */
|
||||||
|
export default function LoadingSpinner() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-32">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
useState,
|
useState,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useMemo,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { X, CheckCircle, AlertCircle, Info } from "lucide-react";
|
import { X, CheckCircle, AlertCircle, Info } from "lucide-react";
|
||||||
@ -52,8 +53,11 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
|||||||
}, 3500);
|
}, 3500);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 🚀 稳定 Context value 引用
|
||||||
|
const ctxValue = useMemo<ToastContextValue>(() => ({ toast }), [toast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastContext.Provider value={{ toast }}>
|
<ToastContext.Provider value={ctxValue}>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{/* Toast 渲染区 */}
|
{/* Toast 渲染区 */}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
useState,
|
useState,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useMemo,
|
||||||
useEffect,
|
useEffect,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
@ -107,15 +108,19 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
logoutInternal();
|
logoutInternal();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 🚀 稳定 Context value 引用,避免消费者不必要的 re-render
|
||||||
|
const ctxValue = useMemo<AuthContextValue>(
|
||||||
|
() => ({
|
||||||
|
...state,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
isAuthenticated: !!state.token && !!state.user,
|
||||||
|
}),
|
||||||
|
[state, login, logout],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider value={ctxValue}>
|
||||||
value={{
|
|
||||||
...state,
|
|
||||||
login,
|
|
||||||
logout,
|
|
||||||
isAuthenticated: !!state.token && !!state.user,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,6 +7,19 @@ import basicSsl from "@vitejs/plugin-basic-ssl";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss(), basicSsl()],
|
plugins: [react(), tailwindcss(), basicSsl()],
|
||||||
|
|
||||||
|
// 🚀 构建优化:将第三方巨头独立分包,利用浏览器缓存
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
"vendor-react": ["react", "react-dom", "react-router-dom"],
|
||||||
|
"vendor-antd": ["antd", "@ant-design/icons"],
|
||||||
|
"vendor-qrcode": ["html5-qrcode"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user