yuthon_base/project_md/static/src/js/mindmap_renderer.js
2026-07-16 13:37:10 +08:00

166 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @odoo-module **/
import { Component, onMounted, onWillUpdateProps } from "@odoo/owl";
export class MindmapRenderer extends Component {
setup() {
super.setup();
}
get treeData() {
return this.props.treeData;
}
onMounted() {
this._renderTree();
}
onWillUpdateProps() {
// 等待 owl 完成 DOM patch 后再重绘 d3避免直接操作 DOM 与 owl 冲突
Promise.resolve().then(() => this._renderTree());
}
_renderTree() {
const d3 = window.d3;
if (!d3) {
console.error("[Mindmap] D3.js not loaded");
return;
}
const container = this.el?.querySelector?.(".mindmap-renderer-root");
if (!container || !this.treeData) return;
const width = Math.max(container.clientWidth, 900);
const height = 560;
// 清空后重绘
container.innerHTML = "";
const svg = d3
.select(container)
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("class", "mindmap-svg");
const g = svg.append("g").attr("class", "mindmap-zoom-group");
const zoom = d3
.zoom()
.scaleExtent([0.15, 4])
.on("zoom", (event) => {
g.attr("transform", event.transform);
});
svg.call(zoom);
const root = d3.hierarchy(this.treeData);
root.descendants().forEach((d) => {
d.dataId = d.data.id;
});
const treeLayout = d3.tree().nodeSize([50, 220]);
treeLayout(root);
// 竖直居中
const x0 = d3.min(root.descendants(), (d) => d.x) || 0;
const x1 = d3.max(root.descendants(), (d) => d.x) || 0;
const initialTransform = d3.zoomIdentity.translate(
80,
(height - (x1 + x0)) / 2
);
svg.call(zoom.transform, initialTransform);
// 连接线
g.selectAll(".mindmap-link")
.data(root.links())
.enter()
.append("path")
.attr("class", "mindmap-link")
.attr("fill", "none")
.attr("stroke", "#90A4AE")
.attr("stroke-width", 1.5)
.attr("opacity", 0.6)
.attr("d", (d) => {
return `M${d.source.y},${d.source.x}
C${(d.source.y + d.target.y) / 2},${d.source.x}
${(d.source.y + d.target.y) / 2},${d.target.x}
${d.target.y},${d.target.x}`;
});
// 节点
const nodes = g
.selectAll(".mindmap-node")
.data(root.descendants())
.enter()
.append("g")
.attr("class", "mindmap-node")
.attr("transform", (d) => `translate(${d.y},${d.x})`)
.style("cursor", "pointer");
nodes
.append("rect")
.attr("rx", 6)
.attr("ry", 6)
.attr("width", 160)
.attr("height", 36)
.attr("x", -80)
.attr("y", -18)
.attr("fill", (d) => (d.children ? "#1E88E5" : "#43A047"))
.attr("stroke", (d) => (d.children ? "#1565C0" : "#2E7D32"))
.attr("stroke-width", 1.5)
.style("filter", "drop-shadow(1px 2px 3px rgba(0,0,0,0.2))")
.on("mouseenter", function () {
d3.select(this)
.transition()
.duration(150)
.attr("stroke-width", 2.5)
.style("filter", "drop-shadow(2px 4px 6px rgba(0,0,0,0.35))");
})
.on("mouseleave", function () {
d3.select(this)
.transition()
.duration(150)
.attr("stroke-width", 1.5)
.style("filter", "drop-shadow(1px 2px 3px rgba(0,0,0,0.2))");
});
nodes
.append("text")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr("fill", "#fff")
.style("font-size", "13px")
.style("font-weight", "500")
.style("pointer-events", "none")
.text((d) => {
const name = d.data.name || "";
return name.length > 14 ? name.substring(0, 13) + "…" : name;
});
// 进度环(有进度的叶子任务)
nodes
.filter((d) => !d.children && d.data.progress && d.data.progress > 0)
.append("circle")
.attr("cx", 66)
.attr("cy", -14)
.attr("r", 10)
.attr("fill", "none")
.attr("stroke", "#FF9800")
.attr("stroke-width", 2.5)
.each(function (d) {
const r = 10;
const circ = 2 * Math.PI * r;
const p = d.data.progress / 100;
d3.select(this)
.attr("stroke-dasharray", circ)
.attr("stroke-dashoffset", circ * (1 - p));
});
// 点击 → 冒泡事件给 Controller
nodes.on("click", (event, d) => {
if (d.data.id) {
this.trigger("node-clicked", { id: d.data.id });
}
});
}
}
MindmapRenderer.template = "project_md.MindmapRenderer";