feat: 焦点/全景模式双翼flex-col纵向堆叠+SVG箭头+实线虚线区分

This commit is contained in:
2026-08-10 13:26:31 +08:00
parent 87296af139
commit fba3589d0d

View File

@ -14,6 +14,10 @@ import type { ModalTarget } from "./TaskTreeViewer";
function fmtTime(d: string | null) { if (!d) return ""; const dt = new Date(d); const pad = (n: number) => String(n).padStart(2, "0"); return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`; }
function isMain(t: TaskResponse) { return !t.parent_task_id || t.task_type === "TRANSFER" || t.task_type === "RECOVERY" || (!t.task_type && !!t.parent_task_id); }
function active(s: string) { return s === "WIP" || s === "PENDING"; }
/** 微型右箭头 SVG */
function ArrowRight({ color = "#9ca3af" }: { color?: string }) {
return <svg className="h-3 w-3 shrink-0" viewBox="0 0 8 8"><polygon points="0,0 8,4 0,8" fill={color} /></svg>;
}
function parseImages(s: string | null | undefined): string[] { if (!s) return []; try { return JSON.parse(s); } catch { return []; } }
function imageUrl(u: string) { if (!u) return ""; return u.startsWith("http") ? u : import.meta.env.VITE_API_BASE_URL + (u.startsWith("/") ? u : "/" + u); }
@ -121,43 +125,50 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren
{/* ─── 焦点模式 ─── */}
{!showFullMap && (
<div className="flex flex-col items-center gap-6">
<div className="flex flex-col items-center gap-4">
{legacySubs.length > 0 && (
<div className="rounded-lg border border-dashed border-orange-200 bg-orange-50 px-3 py-1.5 text-center">
<p className="text-[10px] font-medium text-orange-600"> {legacySubs.length} </p>
</div>
)}
{/* 每个活跃主线一行:左翼 + 中央 + 右翼 */}
{activeMain.map(mainTask => {
const allSubs = [...activeSubs, ...legacySubs];
const leftSubs = allSubs.filter((_, i) => i % 2 === 0);
const rightSubs = allSubs.filter((_, i) => i % 2 === 1);
return (
<div key={mainTask.id} className="flex flex-row items-center w-full">
{/* 左翼 */}
<div className="flex-1 flex justify-end items-center pr-4 gap-1">
<div key={mainTask.id} className="flex flex-row items-start w-full">
{/* 左翼flex-col 纵向堆叠,右对齐 */}
<div className="flex-1 flex flex-col items-end justify-center gap-2 pr-2">
{leftSubs.map(t => {
const isLegacy = legacySubs.includes(t);
return (
<div key={t.id} className="flex items-center">
<SlimCard task={t} assigneeName={assigneeNames?.[t.assignee_id || ""]} active legacy={isLegacy} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} />
<div className={`w-6 border-t-2 ${isLegacy ? "border-dashed border-orange-300" : "border-solid border-gray-400"}`} />
<SlimCard task={t} active legacy={isLegacy} assigneeName={assigneeNames?.[t.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} />
{/* 连线+箭头 → 右侧指向中央 */}
<div className="flex items-center">
<div className={`w-5 border-t-2 ${isLegacy ? "border-dashed border-orange-300" : "border-solid border-gray-400"}`} />
<ArrowRight color={isLegacy ? "#fdba74" : "#9ca3af"} />
</div>
</div>
);
})}
</div>
{/* 中央主干 */}
<div className="shrink-0 z-10">
<SlimCard task={mainTask} assigneeName={assigneeNames?.[mainTask.assignee_id || ""]} active onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} />
<SlimCard task={mainTask} active assigneeName={assigneeNames?.[mainTask.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} />
</div>
{/* 右翼 */}
<div className="flex-1 flex justify-start items-center pl-4 gap-1">
{/* 右翼flex-col 纵向堆叠,左对齐 */}
<div className="flex-1 flex flex-col items-start justify-center gap-2 pl-2">
{rightSubs.map(t => {
const isLegacy = legacySubs.includes(t);
return (
<div key={t.id} className="flex items-center">
<div className={`w-6 border-t-2 ${isLegacy ? "border-dashed border-orange-300" : "border-solid border-gray-400"}`} />
<SlimCard task={t} assigneeName={assigneeNames?.[t.assignee_id || ""]} active legacy={isLegacy} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} />
{/* 连线+箭头 → 右侧指向外侧 */}
<div className="flex items-center">
<ArrowRight color={isLegacy ? "#fdba74" : "#9ca3af"} />
<div className={`w-5 border-t-2 ${isLegacy ? "border-dashed border-orange-300" : "border-solid border-gray-400"}`} />
</div>
<SlimCard task={t} active legacy={isLegacy} assigneeName={assigneeNames?.[t.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} />
</div>
);
})}
@ -177,47 +188,50 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren
const hasLegacyActive = children.some(c => !isMain(c) && !active(mainTask.status));
const leftChildren = children.filter((_, i) => i % 2 === 0);
const rightChildren = children.filter((_, i) => i % 2 === 1);
const lineStyle = hasLegacyActive && !active(mainTask.status);
return (
<div key={mainTask.id} className="relative">
{/* 垂直轴线贯穿中央 */}
<div className="absolute left-1/2 top-0 bottom-0 w-0.5 bg-gray-200 -translate-x-1/2 z-0" />
{/* 水平一行:左翼 + 中央 + 右翼 */}
<div className="flex flex-row items-center w-full">
{/* 左翼分支 */}
<div className="flex-1 flex justify-end items-center pr-4 gap-1">
<div className="flex flex-row items-start w-full">
{/* 左翼flex-col */}
<div className="flex-1 flex flex-col items-end justify-center gap-2 pr-2">
{leftChildren.map(c => (
<div key={c.id} className="flex items-center">
<SlimCard task={c} assigneeName={assigneeNames?.[c.assignee_id || ""]}
active={active(c.status)} legacy={hasLegacyActive && !active(mainTask.status)}
onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} />
<div className={`w-6 border-t-2 ${hasLegacyActive && !active(mainTask.status) ? "border-dashed border-orange-300" : "border-solid border-gray-400"}`} />
<SlimCard task={c} active={active(c.status)} legacy={lineStyle}
assigneeName={assigneeNames?.[c.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} />
<div className="flex items-center">
<div className={`w-5 border-t-2 ${lineStyle ? "border-dashed border-orange-300" : "border-solid border-gray-400"}`} />
<ArrowRight color={lineStyle ? "#fdba74" : "#9ca3af"} />
</div>
</div>
))}
</div>
{/* 中央主干 */}
{/* 中央 */}
<div className="shrink-0 z-10 relative">
<SlimCard task={mainTask} assigneeName={assigneeNames?.[mainTask.assignee_id || ""]}
active={active(mainTask.status)} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} />
<SlimCard task={mainTask} active={active(mainTask.status)}
assigneeName={assigneeNames?.[mainTask.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} />
{active(mainTask.status) && (
<div className="absolute -top-1 -left-1 h-3 w-3 rounded-full bg-green-400 border-2 border-white" />
)}
</div>
{/* 右翼分支 */}
<div className="flex-1 flex justify-start items-center pl-4 gap-1">
{/* 右翼flex-col */}
<div className="flex-1 flex flex-col items-start justify-center gap-2 pl-2">
{rightChildren.map(c => (
<div key={c.id} className="flex items-center">
<div className={`w-6 border-t-2 ${hasLegacyActive && !active(mainTask.status) ? "border-dashed border-orange-300" : "border-solid border-gray-400"}`} />
<SlimCard task={c} assigneeName={assigneeNames?.[c.assignee_id || ""]}
active={active(c.status)} legacy={hasLegacyActive && !active(mainTask.status)}
onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} />
<div className="flex items-center">
<ArrowRight color={lineStyle ? "#fdba74" : "#9ca3af"} />
<div className={`w-5 border-t-2 ${lineStyle ? "border-dashed border-orange-300" : "border-solid border-gray-400"}`} />
</div>
<SlimCard task={c} active={active(c.status)} legacy={lineStyle}
assigneeName={assigneeNames?.[c.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} />
</div>
))}
</div>
</div>
{/* 区间连接 ▼ */}
{allMains.indexOf(mainTask) < allMains.length - 1 && (
<div className="flex justify-center py-2">
<span className="text-[10px] text-gray-300"></span>