feat(web): TaskFlowView 递归渲染重构
- 废弃拍平算法,构建全局childMap保留真实树结构 - 新增renderBranch递归渲染函数,子子孙孙向外延伸 - 焦点/全景模式统一应用递归算法,连线从真实父节点引出
This commit is contained in:
@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user