diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 819c89e..52111f5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { ToastProvider } from "./components/ui/Toast"; import AppLayout from "./components/layout/AppLayout"; import ScanPage from "./pages/ScanPage"; import MyTasksPage from "./pages/MyTasksPage"; @@ -9,27 +10,31 @@ import ProfilePage from "./pages/ProfilePage"; import AdminLayout from "./components/layout/AdminLayout"; import AdminDashboard from "./pages/admin/AdminDashboard"; import AdminProductsPage from "./pages/admin/AdminProductsPage"; +import AdminTasksPage from "./pages/admin/AdminTasksPage"; export default function App() { return ( - - - {/* 移动端 */} - } /> - }> - } /> - } /> - } /> - } /> - + + + + {/* 移动端 */} + } /> + }> + } /> + } /> + } /> + } /> + - {/* PC 管理端 */} - } /> - }> - } /> - } /> - - - + {/* PC 管理端 */} + } /> + }> + } /> + } /> + } /> + + + + ); } diff --git a/frontend/src/components/TaskTree/TaskTreeViewer.tsx b/frontend/src/components/TaskTree/TaskTreeViewer.tsx new file mode 100644 index 0000000..f0cba09 --- /dev/null +++ b/frontend/src/components/TaskTree/TaskTreeViewer.tsx @@ -0,0 +1,952 @@ +import { useState, useCallback } from "react"; +import { + Search, + Loader2, + AlertCircle, + GitBranch, + RefreshCw, + X, + Plus, + Warehouse, + UserPlus, +} from "lucide-react"; +import { + getTaskTree, + receiveTask, + rejectTask, + transferTask, +} from "../../services/taskApi"; +import { useToast } from "../ui/Toast"; +import type { ProductScanResponse, TaskResponse, TaskStatus } from "../../types/api"; +import { TASK_STATUS } from "../../types/api"; + +// ============================================================ +// 状态 → 颜色/标签映射 +// ============================================================ + +const STATUS_CONFIG: Record< + string, + { bg: string; text: string; ring: string; label: string } +> = { + [TASK_STATUS.PENDING]: { + bg: "bg-yellow-50", + text: "text-yellow-700", + ring: "ring-yellow-400", + label: "待接收", + }, + [TASK_STATUS.WIP]: { + bg: "bg-blue-50", + text: "text-blue-700", + ring: "ring-blue-400", + label: "进行中", + }, + [TASK_STATUS.COMPLETED]: { + bg: "bg-green-50", + text: "text-green-700", + ring: "ring-green-400", + label: "已完成", + }, + [TASK_STATUS.REJECTED]: { + bg: "bg-red-50", + text: "text-red-700", + ring: "ring-red-400", + label: "已驳回", + }, + [TASK_STATUS.ARCHIVED]: { + bg: "bg-gray-50", + text: "text-gray-600", + ring: "ring-gray-300", + label: "已入库", + }, +}; + +function getStatusConfig(status: string) { + return ( + STATUS_CONFIG[status] ?? { + bg: "bg-gray-50", + text: "text-gray-600", + ring: "ring-gray-300", + label: status, + } + ); +} + +// ============================================================ +// 通用 Modal 容器 +// ============================================================ + +function Modal({ + open, + onClose, + title, + children, +}: { + open: boolean; + onClose: () => void; + title: string; + children: React.ReactNode; +}) { + if (!open) return null; + + return ( +
+ {/* 遮罩 */} +
+ {/* 弹窗 */} +
+
+

{title}

+ +
+ {children} +
+
+ ); +} + +// ============================================================ +// 确认接收弹窗 +// ============================================================ + +function ReceiveConfirmModal({ + open, + task, + submitting, + onClose, + onConfirm, +}: { + open: boolean; + task: TaskResponse | null; + submitting: boolean; + onClose: () => void; + onConfirm: () => void; +}) { + if (!task) return null; + + return ( + +
+

{task.task_name}

+

+ 当前状态: {getStatusConfig(task.status).label} + {" → "} + 进行中 (WIP) +

+
+

+ 确认接收后,该任务将标记为"进行中",系统将记录接收时间。 +

+
+ + +
+
+ ); +} + +// ============================================================ +// 品质驳回弹窗 +// ============================================================ + +function RejectModal({ + open, + task, + submitting, + onClose, + onSubmit, +}: { + open: boolean; + task: TaskResponse | null; + submitting: boolean; + onClose: () => void; + onSubmit: (reason: string) => void; +}) { + const [reason, setReason] = useState(""); + + if (!task) return null; + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + const trimmed = reason.trim(); + if (!trimmed) return; + onSubmit(trimmed); + setReason(""); + } + + function handleClose() { + setReason(""); + onClose(); + } + + return ( + +
+

{task.task_name}

+

+ ⚠ 驳回后将自动生成返工任务,分配给上一道工序负责人 +

+
+
+ +