fix(mobile): 修复全览按钮点击后页面卡死的响应式循环bug

treeNodes 计算属性中直接修改了 product.task_tree 原始响应式对象
(设置 x/y/_isMain/_blabel),触发 Vue 2 深度响应式的无限渲染循环。

修复方案:对所有 task 对象进行浅拷贝 { ...t },在副本上设置坐标
和标签属性,切断响应式依赖循环。
This commit is contained in:
2026-08-10 15:00:37 +08:00
parent 3ec244bf2d
commit 5271177d7a

View File

@ -83,12 +83,22 @@ export default {
const CARD_W = 160, CARD_H = 80, GAP_X = 24, GAP_Y = 24; const CARD_W = 160, CARD_H = 80, GAP_X = 24, GAP_Y = 24;
const CENTER_X = 0; // 中央主干 X const CENTER_X = 0; // 中央主干 X
// 拍平 + 深拷贝 // 🔧 浅拷贝所有 task 对象,避免修改响应式原始数据导致无限渲染循环
const flatMap = {}; const flatMap = {};
const allTasks = []; const allTasks = [];
const walk = (tasks) => { if (!tasks) return; tasks.forEach(t => { allTasks.push(t); flatMap[t.id] = t; walk(t.child_tasks); }); }; const walk = (tasks) => { if (!tasks) return; tasks.forEach(t => { const copy = { ...t }; allTasks.push(copy); flatMap[copy.id] = copy; walk(t.child_tasks); }); };
walk(this.product.task_tree); walk(this.product.task_tree);
// 重建父子引用child_tasks 指向副本)
allTasks.forEach(t => {
if (t.child_tasks && t.child_tasks.length) {
t.child_tasks = t.child_tasks.map(c => flatMap[c.id]).filter(Boolean);
}
if (t.parent_task_id) {
t.parent = flatMap[t.parent_task_id] || null;
}
});
// 分支编号 // 分支编号
const branchLabelMap = {}; const branchLabelMap = {};
const traverse = (tasks, prefix) => { const traverse = (tasks, prefix) => {
@ -101,7 +111,9 @@ export default {
else { si++; const n = prefix ? prefix + '.' + si : '' + si; branchLabelMap[t.id] = '分支 ' + n; traverse(t.child_tasks, n); } else { si++; const n = prefix ? prefix + '.' + si : '' + si; branchLabelMap[t.id] = '分支 ' + n; traverse(t.child_tasks, n); }
}); });
}; };
this.product.task_tree.forEach(t => { branchLabelMap[t.id] = '主分支'; traverse(t.child_tasks, ''); }); // 遍历顶层时使用副本引用
const rootCopies = this.product.task_tree.map(t => flatMap[t.id]).filter(Boolean);
rootCopies.forEach(t => { branchLabelMap[t.id] = '主分支'; traverse(t.child_tasks, ''); });
// 主管分类 // 主管分类
const mains = []; const subs = []; const subByParent = {}; const mains = []; const subs = []; const subByParent = {};
@ -117,25 +129,27 @@ export default {
const nodes = []; const nodes = [];
const childSeen = new Set(); const childSeen = new Set();
mains.forEach((m, mi) => { mains.forEach((m, mi) => {
m.x = CENTER_X; m.y = mi * (CARD_H + GAP_Y) * 2; const node = { ...m, x: CENTER_X, y: mi * (CARD_H + GAP_Y) * 2, _isMain: true, _blabel: '主分支' };
m._isMain = true; m._blabel = '主分支'; nodes.push(node);
nodes.push(m); // 更新 flatMap 引用以便 lines/arrows 计算使用正确的坐标
flatMap[node.id] = node;
const children = subByParent[m.id] || []; const children = subByParent[m.id] || [];
children.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); children.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
children.forEach((c, ci) => { children.forEach((c, ci) => {
const isLeft = ci % 2 === 0; const isLeft = ci % 2 === 0;
const dist = Math.floor(ci / 2) + 1; const dist = Math.floor(ci / 2) + 1;
c.y = m.y; const childNode = { ...c, y: node.y,
c.x = isLeft ? CENTER_X - dist * (CARD_W + GAP_X) : CENTER_X + dist * (CARD_W + GAP_X); x: isLeft ? CENTER_X - dist * (CARD_W + GAP_X) : CENTER_X + dist * (CARD_W + GAP_X),
c._isMain = false; c._blabel = branchLabelMap[c.id] || '分支'; _isMain: false, _blabel: branchLabelMap[c.id] || '分支' };
nodes.push(c); nodes.push(childNode);
flatMap[childNode.id] = childNode;
childSeen.add(c.id); childSeen.add(c.id);
}); });
}); });
// 遗留分支(父任务不在 mains 中的) // 遗留分支(父任务不在 mains 中的)
subs.forEach(s => { if (!childSeen.has(s.id)) { s.x = 200; s.y = nodes.length ? nodes[nodes.length - 1].y + CARD_H + GAP_Y : 0; s._isMain = false; s._blabel = '分支'; nodes.push(s); } }); subs.forEach(s => { if (!childSeen.has(s.id)) { const legacyNode = { ...s, x: 200, y: nodes.length ? nodes[nodes.length - 1].y + CARD_H + GAP_Y : 0, _isMain: false, _blabel: '分支' }; nodes.push(legacyNode); flatMap[legacyNode.id] = legacyNode; } });
// 加上全局偏移 // 加上全局偏移
const padding = 40; const padding = 40;