feat: 焦点/全景模式双翼flex-col纵向堆叠+SVG箭头+实线虚线区分
This commit is contained in:
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user