feat(web): TaskFlowView 递归渲染重构

- 废弃拍平算法,构建全局childMap保留真实树结构
- 新增renderBranch递归渲染函数,子子孙孙向外延伸
- 焦点/全景模式统一应用递归算法,连线从真实父节点引出
This commit is contained in:
2026-08-11 10:02:06 +08:00
parent 718e205f53
commit d3def15bb3

View File

@ -109,7 +109,7 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren
const [recordsTask, setRecordsTask] = useState<TaskResponse | null>(null);
// 数据分类 — 🚀 虚拟扁平化:三层+嵌套协助统一挂在视觉主干下
const { activeMain, historicalMain, subsByRootMain, rootMainMap } = useMemo(() => {
const { activeMain, historicalMain, childMap, rootMainMap } = useMemo(() => {
ALL_TASKS.clear(); if (tasks.length) collectAll(tasks);
const all = Array.from(ALL_TASKS);
// 拍平映射
@ -136,34 +136,42 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren
// 排序
am.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
hm.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
// 🚀 层级排序:直接子节点优先,嵌套子节点跟在父节点之后
for (const key of Object.keys(srm)) {
const subs = srm[key];
// 构建父子索引
const childMap: Record<string, TaskResponse[]> = {};
for (const s of subs) {
const pid = s.parent_task_id || '';
if (!childMap[pid]) childMap[pid] = [];
childMap[pid].push(s);
}
Object.values(childMap).forEach(arr => arr.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()));
// 递归收集:父 → 子 → 孙
const ordered: TaskResponse[] = [];
const collect = (parentId: string) => {
const kids = childMap[parentId] || [];
for (const k of kids) {
ordered.push(k);
collect(k.id);
}
};
collect(key); // 从根主干开始收集
srm[key] = ordered;
// 🚀 构建全局 childMap(按 parent_task_id 索引直接子节点,保留真实树结构)
const childMap: Record<string, TaskResponse[]> = {};
for (const t of all) {
const pid = t.parent_task_id || '';
if (!childMap[pid]) childMap[pid] = [];
childMap[pid].push(t);
}
return { activeMain: am, historicalMain: hm, subsByRootMain: srm, rootMainMap: rootMap };
Object.values(childMap).forEach(arr => arr.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()));
return { activeMain: am, historicalMain: hm, childMap, rootMainMap: rootMap };
}, [tasks]);
const allMains = [...historicalMain, ...activeMain];
// 🚀 递归渲染分支节点 — 每个节点从自己的 childMap 获取直系子孙,保持树结构不断裂
const renderBranch = (node: TaskResponse, side: 'left' | 'right', isLegacy: boolean, rootMainId: string): JSX.Element => {
const kids = childMap[node.id] || [];
const arrow = side === 'left'
? (<div className="flex items-center"><ArrowLeft color={isLegacy ? "#fdba74" : "#9ca3af"} /><div className={`w-6 border-t-2 ${isLegacy ? "border-dashed border-orange-300" : "border-solid border-gray-400"}`} /></div>)
: (<div className="flex items-center"><div className={`w-6 border-t-2 ${isLegacy ? "border-dashed border-orange-300" : "border-solid border-gray-400"}`} /><ArrowRight color={isLegacy ? "#fdba74" : "#9ca3af"} /></div>);
const card = <SlimCard task={node} active={active(node.status)} legacy={isLegacy} assigneeName={assigneeNames?.[node.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} rootMainId={rootMainId} />;
const kidsContainer = kids.length > 0 ? (
<div className={`flex flex-col gap-2 ${side === 'left' ? 'items-end' : 'items-start'}`}>
{kids.map(k => renderBranch(k, side, isLegacy, rootMainId))}
</div>
) : null;
return (
<div key={node.id} className="flex flex-row items-center gap-1">
{side === 'left' && kidsContainer}
{side === 'left' && card}
{arrow}
{side === 'right' && card}
{side === 'right' && kidsContainer}
</div>
);
};
return (
<div>
{/* 模式切换 */}
@ -178,44 +186,23 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren
{!showFullMap && (
<div className="flex flex-col items-center gap-4">
{activeMain.map(mainTask => {
const children = subsByRootMain[mainTask.id] || [];
const leftSubs = children.filter((_, i) => i % 2 === 0);
const rightSubs = children.filter((_, i) => i % 2 === 1);
const directChildren = childMap[mainTask.id] || [];
const leftDirect = directChildren.filter((_, i) => i % 2 === 0);
const rightDirect = directChildren.filter((_, i) => i % 2 === 1);
const isLegacy = !active(mainTask.status);
return (
<div key={mainTask.id} className="flex flex-row items-start w-full">
{/* 左翼 */}
{/* 左翼 — 递归渲染,子子孙孙向外延伸 */}
<div className="flex-1 flex flex-col items-end justify-center gap-2 pr-2">
{leftSubs.map(t => {
const isLegacy = !active(mainTask.status);
return (
<div key={t.id} className="flex items-center">
<SlimCard task={t} active legacy={isLegacy} assigneeName={assigneeNames?.[t.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} rootMainId={mainTask.id} />
<div className="flex items-center">
<ArrowLeft color={isLegacy ? "#fdba74" : "#9ca3af"} />
<div className={`w-5 border-t-2 ${isLegacy ? "border-dashed border-orange-300" : "border-solid border-gray-400"}`} />
</div>
</div>
);
})}
{leftDirect.map(t => renderBranch(t, 'left', isLegacy, mainTask.id))}
</div>
{/* 中央主干 */}
<div className="shrink-0 z-10">
<SlimCard task={mainTask} active assigneeName={assigneeNames?.[mainTask.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} />
</div>
{/* 右翼 */}
{/* 右翼 — 递归渲染,子子孙孙向外延伸 */}
<div className="flex-1 flex flex-col items-start justify-center gap-2 pl-2">
{rightSubs.map(t => {
const isLegacy = !active(mainTask.status);
return (
<div key={t.id} className="flex items-center">
<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>
<SlimCard task={t} active legacy={isLegacy} assigneeName={assigneeNames?.[t.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} rootMainId={mainTask.id} />
</div>
);
})}
{rightDirect.map(t => renderBranch(t, 'right', isLegacy, mainTask.id))}
</div>
</div>
);
@ -228,28 +215,19 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren
{showFullMap && (
<div className="space-y-6">
{allMains.map(mainTask => {
const children = subsByRootMain[mainTask.id] || [];
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 directChildren = childMap[mainTask.id] || [];
const hasLegacyActive = directChildren.some(c => !isMain(c) && !active(mainTask.status));
const leftDirect = directChildren.filter((_, i) => i % 2 === 0);
const rightDirect = directChildren.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-start w-full">
{/* 左翼 */}
{/* 左翼 — 递归渲染,子子孙孙向外延伸 */}
<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} active={active(c.status)} legacy={lineStyle}
assigneeName={assigneeNames?.[c.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} rootMainId={mainTask.id} />
<div className="flex items-center">
<ArrowLeft color={lineStyle ? "#fdba74" : "#9ca3af"} />
<div className={`w-5 border-t-2 ${lineStyle ? "border-dashed border-orange-300" : "border-solid border-gray-400"}`} />
</div>
</div>
))}
{leftDirect.map(c => renderBranch(c, 'left', lineStyle, mainTask.id))}
</div>
{/* 中央 */}
<div className="shrink-0 z-10 relative">
@ -259,18 +237,9 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren
<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 flex-col items-start justify-center gap-2 pl-2">
{rightChildren.map(c => (
<div key={c.id} className="flex items-center">
<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>
<SlimCard task={c} active={active(c.status)} legacy={lineStyle}
assigneeName={assigneeNames?.[c.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} rootMainId={mainTask.id} />
</div>
))}
{rightDirect.map(c => renderBranch(c, 'right', lineStyle, mainTask.id))}
</div>
</div>