diff --git a/frontend/src/components/TaskTree/TaskFlowView.tsx b/frontend/src/components/TaskTree/TaskFlowView.tsx index 332c59e..2f9fb00 100644 --- a/frontend/src/components/TaskTree/TaskFlowView.tsx +++ b/frontend/src/components/TaskTree/TaskFlowView.tsx @@ -2,7 +2,7 @@ * 流转树双模式可视化 — 焦点模式 + 全景模式 */ import { memo, useMemo, useState } from "react"; -import { GitBranch, AlertTriangle, Clock, CheckCircle, Flag, FileText, X } from "lucide-react"; +import { FileText, X } from "lucide-react"; import type { TaskResponse } from "../../types/api"; import { TASK_STATUS } from "../../types/api"; import { getStatusConfig } from "../../constants/task"; @@ -15,12 +15,12 @@ function fmtTime(d: string | null) { if (!d) return ""; const dt = new Date(d); function isMain(t: TaskResponse) { return !t.parent_task_id || t.task_type === "TRANSFER" || t.task_type === "RECOVERY"; } function active(s: string) { return s === "WIP" || s === "PENDING"; } /** 微型右箭头 SVG */ -function ArrowRight({ color = "#9ca3af" }: { color?: string }) { - return ; +function ArrowRight({ color = "#9ca3af", big }: { color?: string; big?: boolean }) { + return ; } /** 微型左箭头 SVG */ -function ArrowLeft({ color = "#9ca3af" }: { color?: string }) { - return ; +function ArrowLeft({ color = "#9ca3af", big }: { color?: string; big?: boolean }) { + return ; } function parseImages(s: any): string[] { if (!s) return []; @@ -48,56 +48,92 @@ const ALL_TASKS = new Set(); function collectAll(tasks: TaskResponse[]) { tasks.forEach(t => { ALL_TASKS.add(t); if (t.child_tasks) collectAll(t.child_tasks); }); } function findParent(child: TaskResponse): TaskResponse | undefined { for (const t of ALL_TASKS) { if (t.id === child.parent_task_id) return t; } return undefined; } +// ============================================================ +// 尺寸映射表(sm=现状小卡片,lg=弹窗放大) +// ============================================================ +const SIZE_MAP = { + sm: { + card: "w-44 p-2.5 shadow-sm", + badge: "-top-1.5 right-2 px-1.5 py-px text-[8px]", + title: "mt-1 text-xs", + metaRow: "mt-1", + status: "px-1.5 py-px text-[8px]", + assignee: "text-[9px]", + time: "mt-1 text-[8px]", + sub: "mt-1 text-[8px]", + btnRow: "mt-1.5 pt-1.5", + btn: "py-0.5 text-[8px]", + record: "mt-1 px-1.5 py-0.5 text-[8px]", + recordIcon: "h-2.5 w-2.5", + }, + lg: { + card: "w-72 p-4 shadow-md", + badge: "-top-2 right-3 px-2 py-0.5 text-xs", + title: "mt-1.5 text-base", + metaRow: "mt-2", + status: "px-2 py-0.5 text-xs", + assignee: "text-sm", + time: "mt-2 text-xs", + sub: "mt-1.5 text-xs", + btnRow: "mt-2.5 pt-2.5", + btn: "py-1.5 text-sm", + record: "mt-2 px-2.5 py-1 text-xs", + recordIcon: "h-4 w-4", + }, +} as const; + // ============================================================ // 极简卡片 // ============================================================ const SlimCard = memo(function SlimCard({ - task, assigneeName, active, legacy, onAction, currentUser, onViewRecords, rootMainId, + task, assigneeName, active, legacy, onAction, currentUser, onViewRecords, rootMainId, size = "sm", }: { task: TaskResponse; assigneeName?: string; active: boolean; legacy?: boolean; onAction: (t: ModalTarget) => void; currentUser?: { username?: string; role?: string } | null; onViewRecords?: (t: TaskResponse) => void; rootMainId?: string; + size?: "sm" | "lg"; }) { const cfg = getStatusConfig(task.status); + const sz = SIZE_MAP[size]; const isOwner = !!(currentUser?.username && task.assignee_id === currentUser.username); const isManager = currentUser?.role === "SUPER_ADMIN" || currentUser?.role === "SUPERVISOR"; const main = isMain(task); const isNestedSpawn = !main && rootMainId && task.parent_task_id !== rootMainId && !!task.parent_task_id; return ( -
-
{main ? "主线" : "分支"}
-

{task.task_name}

-
- {cfg.label} - {assigneeName || task.assignee_id || "—"} +
+
{main ? "主线" : "分支"}
+

{task.task_name}

+
+ {cfg.label} + {assigneeName || task.assignee_id || "—"}
{/* 单行时间 */} -

+

⏰ {fmtTime(task.created_at).split(" ")[0]} {task.completed_at ? ` → ${fmtTime(task.completed_at).split(" ")[0]}` : " → 至今"}

- {legacy &&

源自: {assigneeName || (findParent(task)?.assignee_id) || "历史任务"}

} - {isNestedSpawn &&

协助: {findParent(task)?.assignee_id || "—"}

} + {legacy &&

源自: {assigneeName || (findParent(task)?.assignee_id) || "历史任务"}

} + {isNestedSpawn &&

协助: {findParent(task)?.assignee_id || "—"}

} {/* 操作按钮 */} {active && isOwner && ( -
- {task.status?.toUpperCase() === TASK_STATUS.PENDING && } - {task.status?.toUpperCase() !== TASK_STATUS.PENDING && } - +
+ {task.status?.toUpperCase() === TASK_STATUS.PENDING && } + {task.status?.toUpperCase() !== TASK_STATUS.PENDING && } +
)} {active && !isOwner && isManager && ( -
- - +
+ +
)} {/* 记录 */} {task.records && task.records.length > 0 && ( -
{ e.stopPropagation(); onViewRecords?.(task); }} className="mt-1 cursor-pointer rounded bg-blue-50 px-1.5 py-0.5 text-[8px] text-blue-600 hover:bg-blue-100"> - {task.records.length}条 +
{ e.stopPropagation(); onViewRecords?.(task); }} className={`${sz.record} cursor-pointer rounded bg-blue-50 text-blue-600 hover:bg-blue-100`}> + {task.records.length}条
)}
@@ -111,9 +147,10 @@ interface TaskFlowViewProps { tasks: TaskResponse[]; onAction: (t: ModalTarget) => void; currentUser?: { username?: string; role?: string } | null; assigneeNames?: Record; + size?: "sm" | "lg"; } -export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, currentUser, assigneeNames }: TaskFlowViewProps) { +export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, currentUser, assigneeNames, size = "sm" }: TaskFlowViewProps) { const [showFullMap, setShowFullMap] = useState(false); const [recordsTask, setRecordsTask] = useState(null); @@ -139,12 +176,13 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren }, [tasks]); // 🚀 递归渲染分支节点 — 每个节点从自己的 childMap 获取直系子孙,保持树结构不断裂 - const renderBranch = (node: TaskResponse, side: 'left' | 'right', isLegacy: boolean, rootMainId: string): JSX.Element => { + const renderBranch = (node: TaskResponse, side: 'left' | 'right', isLegacy: boolean, rootMainId: string): React.JSX.Element => { const kids = childMap[node.id] || []; + const big = size === "lg"; const arrow = side === 'left' - ? (
) - : (
); - const card = ; + ? (
) + : (
); + const card = ; const kidsContainer = kids.length > 0 ? (
{kids.map(k => renderBranch(k, side, isLegacy, rootMainId))} @@ -178,7 +216,7 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren {/* 模式切换 */}
@@ -195,7 +233,7 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren return (
-
+
{/* 左翼 — 递归渲染,子子孙孙向外延伸 */}
@@ -204,9 +242,9 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren {/* 中央 */}
+ assigneeName={assigneeNames?.[mainTask.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} size={size} /> {active(mainTask.status) && ( -
+
)}
{/* 右翼 — 递归渲染,子子孙孙向外延伸 */} @@ -217,14 +255,14 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren {visibleMains.indexOf(mainTask) < visibleMains.length - 1 && (
- +
)}
); })} {visibleMains.length === 0 && ( -

+

{showFullMap ? "暂无流转记录" : "当前无活跃主线任务"}

)} @@ -232,17 +270,17 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren {/* 记录弹窗 */} {recordsTask && ( -
+
setRecordsTask(null)} /> -
-

提交记录 — {recordsTask.task_name}

+
+

提交记录 — {recordsTask.task_name}

{(recordsTask.records || []).length === 0 ?

暂无记录

:
{[...recordsTask.records!].reverse().map((r, i) => (
-

{fmtTime(r.created_at)}

- {(r.note || r.remark) &&

{r.note || r.remark}

} - {(() => { const imgs = parseImages(r.images); if (!imgs.length) return null; return
{imgs.map((img, j) => window.open(imageUrl(img))} />)}
; })()} +

{fmtTime(r.created_at)}

+ {r.remark &&

{r.remark}

} + {(() => { const imgs = parseImages(r.images); if (!imgs.length) return null; return
{imgs.map((img, j) => window.open(imageUrl(img))} />)}
; })()}
))}
} diff --git a/frontend/src/components/TaskTree/TaskTreeViewer.tsx b/frontend/src/components/TaskTree/TaskTreeViewer.tsx index c518044..9acf958 100644 --- a/frontend/src/components/TaskTree/TaskTreeViewer.tsx +++ b/frontend/src/components/TaskTree/TaskTreeViewer.tsx @@ -25,16 +25,20 @@ import { getStatusConfig } from "../../constants/task"; // 通用 Modal 容器 // ============================================================ -const Modal = memo(function Modal({ +export const Modal = memo(function Modal({ open, onClose, title, children, + widthClass = "max-w-md", + bodyClassName = "", }: { open: boolean; onClose: () => void; title: string; children: React.ReactNode; + widthClass?: string; + bodyClassName?: string; }) { if (!open) return null; @@ -46,7 +50,7 @@ const Modal = memo(function Modal({ onClick={onClose} /> {/* 弹窗 */} -
+

{title}

); }, @@ -221,9 +220,9 @@ export default function AdminTasksPage() { if (autoSn) { setKeyword(autoSn); loadProducts(autoSn).then((data) => { - // 产品加载完成后自动展开流转树 + // 产品加载完成后自动打开流转树弹窗 const found = data.find((p: ProductResponse) => p.serial_number === autoSn); - if (found) toggleProductTree(found.serial_number); + if (found) openTreeModal(found.serial_number); }); } else { loadProducts(keyword); @@ -234,6 +233,7 @@ export default function AdminTasksPage() { e?.preventDefault(); setExpandedOrders(new Set()); setTaskTrees({}); + setActiveTreeProductId(null); loadProducts(keyword); } @@ -362,14 +362,8 @@ export default function AdminTasksPage() { }); } - // ---- 排他展开(手风琴模式) ---- - async function toggleProductTree(serialNumber: string) { - // 点击已展开的树 → 收起 - if (activeTreeProductId === serialNumber) { - setActiveTreeProductId(null); - return; - } - // 展开新的 → 自动收起旧的 + // ---- 流转树宽屏弹窗(懒加载,缓存到 taskTrees) ---- + function openTreeModal(serialNumber: string) { setActiveTreeProductId(serialNumber); if (!taskTrees[serialNumber]) { setTreeLoading((s) => ({ ...s, [serialNumber]: true })); @@ -379,6 +373,9 @@ export default function AdminTasksPage() { .finally(() => setTreeLoading((s) => ({ ...s, [serialNumber]: false }))); } } + function closeTreeModal() { + setActiveTreeProductId(null); + } // ---- 任务操作 ---- const refreshProductTree = useCallback(async (serialNumber: string) => { @@ -695,10 +692,6 @@ export default function AdminTasksPage() { {/* 产品行 */} {group.products.map((p) => { - const productExpanded = activeTreeProductId === p.serial_number; - const isTreeLoading = treeLoading[p.serial_number]; - const tree = taskTrees[p.serial_number]; - return (
@@ -708,36 +701,6 @@ export default function AdminTasksPage() {
))}
- - {/* 展开的流转树 — 卡片堆叠视图 */} - {productExpanded && tree && ( -
-

- - 流转卡片 — {p.serial_number} -

- {tree.task_tree && tree.task_tree.length > 0 ? ( - - ) : ( -
- -

暂无流转记录

-

产品刚创建,尚未分配生产任务

-
- )} -
- )} - {productExpanded && isTreeLoading && ( -
- -

加载流转树...

-
- )}
); })} @@ -762,6 +725,37 @@ export default function AdminTasksPage() {
)} + {/* 🔧 流转树宽屏弹窗 */} + {activeTreeProductId && ( + + {treeLoading[activeTreeProductId] ? ( +
+ +
+ ) : taskTrees[activeTreeProductId]?.task_tree?.length ? ( + + ) : ( +
+ +

暂无流转记录

+

产品刚创建,尚未分配生产任务

+
+ )} +
+ )} + {/* ---- 弹窗 ---- */}