feat: align with base project's No ESM design and register global Mindmap component (by AI)
This commit is contained in:
parent
85c2dd983d
commit
264911d495
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
test-results/
|
||||
.DS_Store
|
||||
@ -1,5 +1,10 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v1.0.2 (2026-06-12)
|
||||
- **架构对齐**: 遵循 `base` 包新设计,彻底消灭用户侧 ESM 强依赖。
|
||||
- **组件注册**: 支持 `<Mindmap>` 全局组件声明式用法。
|
||||
- **构建优化**: 产物调整为纯 UMD 模式,通过 `globalThis.Mindmap` 暴露 API。
|
||||
|
||||
## v1.0.0 (2026-05-29)
|
||||
- 初始化项目架构。
|
||||
- 实现基础渲染占位。
|
||||
|
||||
79
README.md
79
README.md
@ -1,44 +1,55 @@
|
||||
# @web/mindmap
|
||||
# @apigo.cc/mindmap API 手册
|
||||
|
||||
类 Xmind 体验的思维导图引擎,专注于展示逻辑分支。
|
||||
原生 JS 编写的思维导图引擎。支持无限层级、缩放平移、节点折叠及动态增删。
|
||||
|
||||
## 特性
|
||||
- 原生 ESM 驱动,无需构建即可在浏览器中使用。
|
||||
- 支持自动布局算法。
|
||||
- 高性能 SVG/DOM 渲染(1000+ 节点渲染耗时 < 50ms)。
|
||||
- 支持节点的展开与折叠。
|
||||
- 支持画布平移与缩放。
|
||||
---
|
||||
|
||||
## 安装与使用
|
||||
本模块不发布至 npmjs。请通过 `loader.js` 或 `importmap` 引入。
|
||||
## 1. 引入方式 (UMD 优先)
|
||||
|
||||
### 使用示例
|
||||
```javascript
|
||||
import { Mindmap } from '@web/mindmap';
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/mindmap@1.0.1/dist/mindmap.min.js"></script>
|
||||
|
||||
const mm = new Mindmap({
|
||||
container: document.getElementById('app'),
|
||||
data: {
|
||||
text: '根节点',
|
||||
children: [
|
||||
{ text: '子节点 1' },
|
||||
{ text: '子节点 2' }
|
||||
]
|
||||
}
|
||||
});
|
||||
<script>
|
||||
// 直接使用全局 Mindmap 类
|
||||
const mm = new Mindmap(config);
|
||||
</script>
|
||||
```
|
||||
|
||||
## API
|
||||
---
|
||||
|
||||
## 2. 核心用法
|
||||
|
||||
### 组件化用法 (推荐)
|
||||
直接在 HTML 中使用 `<Mindmap>` 标签,通过 `state` 进行数据绑定。
|
||||
|
||||
```html
|
||||
<div $data="{
|
||||
myMind: { id: 'root', text: '根节点', children: [{ text: '子节点 1' }] }
|
||||
}">
|
||||
<Mindmap $.state.data="myMind" style="height:500px"></Mindmap>
|
||||
</div>
|
||||
```
|
||||
|
||||
### JS 调用方式 (兼容)
|
||||
...
|
||||
---
|
||||
|
||||
## 3. API 参考
|
||||
|
||||
### `new Mindmap(options)`
|
||||
- `options.container`: HTMLElement, 容器元素。
|
||||
- `options.data`: Object, 树形数据结构。
|
||||
- `options.nodeWidth`: Number, 节点宽度 (默认 120)。
|
||||
- `options.nodeHeight`: Number, 节点高度 (默认 40)。
|
||||
- `options.hGap`: Number, 水平间距 (默认 60)。
|
||||
- `options.vGap`: Number, 垂直间距 (默认 20)。
|
||||
- **`container`**: 挂载元素。
|
||||
- **`data`**: 格式为 `{ id, text, children, _collapsed }` 的对象。
|
||||
- **`nodeWidth / nodeHeight`**: 节点尺寸。
|
||||
- **`hGap / vGap`**: 间距。
|
||||
|
||||
### `mm.addNode(parentId, text)`
|
||||
- 向指定 ID 或文本匹配的节点添加子节点。
|
||||
### 实例方法
|
||||
- **`addNode(parentId, text)`**: 动态添加节点。
|
||||
- **`removeNode(nodeId)`**: 动态删除节点。
|
||||
- **`render()`**: 强制重新布局与重绘。
|
||||
|
||||
### `mm.removeNode(nodeId)`
|
||||
- 删除指定 ID 或文本匹配的节点。
|
||||
---
|
||||
|
||||
## 开发者提示 (AI 必读)
|
||||
1. **全局变量**: UMD 模式下,`Mindmap` 类自动挂载到 `window`。
|
||||
2. **唯一 ID**: 每个节点必须拥有全局唯一的 `id`。
|
||||
3. **数据响应**: 修改 `mm.data` 属性后必须手动执行 `mm.render()`。
|
||||
|
||||
425
dist/mindmap.js
vendored
425
dist/mindmap.js
vendored
@ -1,205 +1,232 @@
|
||||
class Mindmap {
|
||||
constructor(options = {}) {
|
||||
this.container = options.container;
|
||||
this.data = options.data || { id: "root", text: "Root", children: [] };
|
||||
this.nodeWidth = options.nodeWidth || 120;
|
||||
this.nodeHeight = options.nodeHeight || 40;
|
||||
this.hGap = options.hGap || 60;
|
||||
this.vGap = options.vGap || 20;
|
||||
this.nodes = [];
|
||||
this.links = [];
|
||||
this.init();
|
||||
}
|
||||
init() {
|
||||
if (!this.container) return;
|
||||
this.container.style.position = "relative";
|
||||
this.container.style.overflow = "hidden";
|
||||
this.container.style.backgroundColor = "#f5f5f5";
|
||||
this.viewport = document.createElement("div");
|
||||
this.viewport.style.position = "absolute";
|
||||
this.viewport.style.width = "10000px";
|
||||
this.viewport.style.height = "10000px";
|
||||
this.viewport.style.transformOrigin = "0 0";
|
||||
this.container.appendChild(this.viewport);
|
||||
this.scale = 1;
|
||||
this.offsetX = this.container.clientWidth / 2 - 100;
|
||||
this.offsetY = this.container.clientHeight / 2 - 50;
|
||||
this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
this.svg.style.position = "absolute";
|
||||
this.svg.style.top = "0";
|
||||
this.svg.style.left = "0";
|
||||
this.svg.style.width = "100%";
|
||||
this.svg.style.height = "100%";
|
||||
this.svg.style.pointerEvents = "none";
|
||||
this.viewport.appendChild(this.svg);
|
||||
this.initEvents();
|
||||
this.updateViewport();
|
||||
this.render();
|
||||
}
|
||||
initEvents() {
|
||||
let isDragging = false;
|
||||
let lastX, lastY;
|
||||
this.container.onmousedown = (e) => {
|
||||
if (e.target.classList.contains("mindmap-node")) return;
|
||||
isDragging = true;
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
};
|
||||
window.onmousemove = (e) => {
|
||||
if (!isDragging) return;
|
||||
this.offsetX += e.clientX - lastX;
|
||||
this.offsetY += e.clientY - lastY;
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
(function(global, factory) {
|
||||
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.ApigoMindmap = {}));
|
||||
})(this, function(exports2) {
|
||||
"use strict";
|
||||
class Mindmap {
|
||||
constructor(options = {}) {
|
||||
this.container = options.container;
|
||||
this.data = options.data || { id: "root", text: "Root", children: [] };
|
||||
this.nodeWidth = options.nodeWidth || 120;
|
||||
this.nodeHeight = options.nodeHeight || 40;
|
||||
this.hGap = options.hGap || 60;
|
||||
this.vGap = options.vGap || 20;
|
||||
this.nodes = [];
|
||||
this.links = [];
|
||||
this.init();
|
||||
}
|
||||
init() {
|
||||
if (!this.container) return;
|
||||
this.container.style.position = "relative";
|
||||
this.container.style.overflow = "hidden";
|
||||
this.container.style.backgroundColor = "#f5f5f5";
|
||||
this.viewport = document.createElement("div");
|
||||
this.viewport.style.position = "absolute";
|
||||
this.viewport.style.width = "10000px";
|
||||
this.viewport.style.height = "10000px";
|
||||
this.viewport.style.transformOrigin = "0 0";
|
||||
this.container.appendChild(this.viewport);
|
||||
this.scale = 1;
|
||||
this.offsetX = this.container.clientWidth / 2 - 100;
|
||||
this.offsetY = this.container.clientHeight / 2 - 50;
|
||||
this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
this.svg.style.position = "absolute";
|
||||
this.svg.style.top = "0";
|
||||
this.svg.style.left = "0";
|
||||
this.svg.style.width = "100%";
|
||||
this.svg.style.height = "100%";
|
||||
this.svg.style.pointerEvents = "none";
|
||||
this.viewport.appendChild(this.svg);
|
||||
this.initEvents();
|
||||
this.updateViewport();
|
||||
};
|
||||
window.onmouseup = () => {
|
||||
isDragging = false;
|
||||
};
|
||||
this.container.onwheel = (e) => {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
const newScale = this.scale * delta;
|
||||
if (newScale < 0.1 || newScale > 5) return;
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
this.offsetX = mouseX - (mouseX - this.offsetX) * delta;
|
||||
this.offsetY = mouseY - (mouseY - this.offsetY) * delta;
|
||||
this.scale = newScale;
|
||||
this.updateViewport();
|
||||
};
|
||||
}
|
||||
updateViewport() {
|
||||
this.viewport.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.scale})`;
|
||||
}
|
||||
layout() {
|
||||
this.nodes = [];
|
||||
this.links = [];
|
||||
const compute = (node, x, startY) => {
|
||||
const isCollapsed = node._collapsed;
|
||||
const childrenHeight = node.children && node.children.length > 0 && !isCollapsed ? node.children.reduce((acc, child) => acc + this.getTotalHeight(child), 0) : this.nodeHeight;
|
||||
const y = startY + childrenHeight / 2 - this.nodeHeight / 2;
|
||||
const nodeInfo = {
|
||||
x,
|
||||
y,
|
||||
text: node.text,
|
||||
id: node.id || Math.random().toString(36).substr(2, 9),
|
||||
raw: node
|
||||
this.render();
|
||||
}
|
||||
initEvents() {
|
||||
let isDragging = false;
|
||||
let lastX, lastY;
|
||||
this.container.onmousedown = (e) => {
|
||||
if (e.target.classList.contains("mindmap-node")) return;
|
||||
isDragging = true;
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
};
|
||||
this.nodes.push(nodeInfo);
|
||||
if (node.children && !isCollapsed) {
|
||||
let currentY = startY;
|
||||
node.children.forEach((child) => {
|
||||
const childInfo = compute(child, x + this.nodeWidth + this.hGap, currentY);
|
||||
this.links.push({
|
||||
x1: x + this.nodeWidth,
|
||||
y1: y + this.nodeHeight / 2,
|
||||
x2: childInfo.x,
|
||||
y2: childInfo.y + this.nodeHeight / 2
|
||||
window.onmousemove = (e) => {
|
||||
if (!isDragging) return;
|
||||
this.offsetX += e.clientX - lastX;
|
||||
this.offsetY += e.clientY - lastY;
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
this.updateViewport();
|
||||
};
|
||||
window.onmouseup = () => {
|
||||
isDragging = false;
|
||||
};
|
||||
this.container.onwheel = (e) => {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
const newScale = this.scale * delta;
|
||||
if (newScale < 0.1 || newScale > 5) return;
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
this.offsetX = mouseX - (mouseX - this.offsetX) * delta;
|
||||
this.offsetY = mouseY - (mouseY - this.offsetY) * delta;
|
||||
this.scale = newScale;
|
||||
this.updateViewport();
|
||||
};
|
||||
}
|
||||
updateViewport() {
|
||||
this.viewport.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.scale})`;
|
||||
}
|
||||
layout() {
|
||||
this.nodes = [];
|
||||
this.links = [];
|
||||
const compute = (node, x, startY) => {
|
||||
const isCollapsed = node._collapsed;
|
||||
const childrenHeight = node.children && node.children.length > 0 && !isCollapsed ? node.children.reduce((acc, child) => acc + this.getTotalHeight(child), 0) : this.nodeHeight;
|
||||
const y = startY + childrenHeight / 2 - this.nodeHeight / 2;
|
||||
const nodeInfo = {
|
||||
x,
|
||||
y,
|
||||
text: node.text,
|
||||
id: node.id || Math.random().toString(36).substr(2, 9),
|
||||
raw: node
|
||||
};
|
||||
this.nodes.push(nodeInfo);
|
||||
if (node.children && !isCollapsed) {
|
||||
let currentY = startY;
|
||||
node.children.forEach((child) => {
|
||||
const childInfo = compute(child, x + this.nodeWidth + this.hGap, currentY);
|
||||
this.links.push({
|
||||
x1: x + this.nodeWidth,
|
||||
y1: y + this.nodeHeight / 2,
|
||||
x2: childInfo.x,
|
||||
y2: childInfo.y + this.nodeHeight / 2
|
||||
});
|
||||
currentY += this.getTotalHeight(child);
|
||||
});
|
||||
currentY += this.getTotalHeight(child);
|
||||
});
|
||||
}
|
||||
return nodeInfo;
|
||||
};
|
||||
compute(this.data, 0, 0);
|
||||
}
|
||||
getTotalHeight(node) {
|
||||
if (!node.children || node.children.length === 0 || node._collapsed) {
|
||||
return this.nodeHeight + this.vGap;
|
||||
}
|
||||
return node.children.reduce((acc, child) => acc + this.getTotalHeight(child), 0);
|
||||
}
|
||||
render() {
|
||||
this.layout();
|
||||
this.svg.innerHTML = "";
|
||||
const existingNodes = this.viewport.querySelectorAll(".mindmap-node");
|
||||
existingNodes.forEach((n) => n.remove());
|
||||
this.links.forEach((link) => {
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
const d = `M ${link.x1} ${link.y1} C ${link.x1 + this.hGap / 2} ${link.y1}, ${link.x2 - this.hGap / 2} ${link.y2}, ${link.x2} ${link.y2}`;
|
||||
path.setAttribute("d", d);
|
||||
path.setAttribute("stroke", "#4A90E2");
|
||||
path.setAttribute("fill", "none");
|
||||
path.setAttribute("stroke-width", "2");
|
||||
this.svg.appendChild(path);
|
||||
});
|
||||
this.nodes.forEach((node) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "mindmap-node";
|
||||
if (node.raw._collapsed) el.classList.add("collapsed");
|
||||
el.style.position = "absolute";
|
||||
el.style.left = `${node.x}px`;
|
||||
el.style.top = `${node.y}px`;
|
||||
el.style.width = `${this.nodeWidth}px`;
|
||||
el.style.height = `${this.nodeHeight}px`;
|
||||
el.style.border = "2px solid #4A90E2";
|
||||
el.style.borderRadius = "8px";
|
||||
el.style.backgroundColor = "#fff";
|
||||
el.style.display = "flex";
|
||||
el.style.alignItems = "center";
|
||||
el.style.justifyContent = "center";
|
||||
el.style.fontSize = "14px";
|
||||
el.style.fontWeight = "bold";
|
||||
el.style.color = "#333";
|
||||
el.style.boxShadow = "0 4px 6px rgba(0,0,0,0.1)";
|
||||
el.style.cursor = "pointer";
|
||||
el.style.userSelect = "none";
|
||||
el.style.transition = "transform 0.2s";
|
||||
el.innerText = node.text;
|
||||
if (node.raw._collapsed) {
|
||||
el.style.backgroundColor = "#f0f0f0";
|
||||
el.style.borderStyle = "dashed";
|
||||
}
|
||||
el.onmouseenter = () => el.style.transform = "scale(1.05)";
|
||||
el.onmouseleave = () => el.style.transform = "scale(1)";
|
||||
el.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
node.raw._collapsed = !node.raw._collapsed;
|
||||
this.render();
|
||||
};
|
||||
this.viewport.appendChild(el);
|
||||
});
|
||||
}
|
||||
addNode(parentId, text) {
|
||||
const findAndAdd = (node) => {
|
||||
if (node.id === parentId || node.text === parentId) {
|
||||
if (!node.children) node.children = [];
|
||||
node.children.push({ text, id: Math.random().toString(36).substr(2, 9) });
|
||||
return true;
|
||||
}
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
if (findAndAdd(child)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
findAndAdd(this.data);
|
||||
this.render();
|
||||
}
|
||||
removeNode(nodeId) {
|
||||
const remove = (node) => {
|
||||
if (!node.children) return false;
|
||||
const index = node.children.findIndex((child) => child.id === nodeId || child.text === nodeId);
|
||||
if (index !== -1) {
|
||||
node.children.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
for (const child of node.children) {
|
||||
if (remove(child)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (this.data.id === nodeId || this.data.text === nodeId) {
|
||||
console.warn("Cannot remove root node");
|
||||
return;
|
||||
return nodeInfo;
|
||||
};
|
||||
compute(this.data, 0, 0);
|
||||
}
|
||||
getTotalHeight(node) {
|
||||
if (!node.children || node.children.length === 0 || node._collapsed) {
|
||||
return this.nodeHeight + this.vGap;
|
||||
}
|
||||
return node.children.reduce((acc, child) => acc + this.getTotalHeight(child), 0);
|
||||
}
|
||||
render() {
|
||||
this.layout();
|
||||
this.svg.innerHTML = "";
|
||||
const existingNodes = this.viewport.querySelectorAll(".mindmap-node");
|
||||
existingNodes.forEach((n) => n.remove());
|
||||
this.links.forEach((link) => {
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
const d = `M ${link.x1} ${link.y1} C ${link.x1 + this.hGap / 2} ${link.y1}, ${link.x2 - this.hGap / 2} ${link.y2}, ${link.x2} ${link.y2}`;
|
||||
path.setAttribute("d", d);
|
||||
path.setAttribute("stroke", "#4A90E2");
|
||||
path.setAttribute("fill", "none");
|
||||
path.setAttribute("stroke-width", "2");
|
||||
this.svg.appendChild(path);
|
||||
});
|
||||
this.nodes.forEach((node) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "mindmap-node";
|
||||
if (node.raw._collapsed) el.classList.add("collapsed");
|
||||
el.style.position = "absolute";
|
||||
el.style.left = `${node.x}px`;
|
||||
el.style.top = `${node.y}px`;
|
||||
el.style.width = `${this.nodeWidth}px`;
|
||||
el.style.height = `${this.nodeHeight}px`;
|
||||
el.style.border = "2px solid #4A90E2";
|
||||
el.style.borderRadius = "8px";
|
||||
el.style.backgroundColor = "#fff";
|
||||
el.style.display = "flex";
|
||||
el.style.alignItems = "center";
|
||||
el.style.justifyContent = "center";
|
||||
el.style.fontSize = "14px";
|
||||
el.style.fontWeight = "bold";
|
||||
el.style.color = "#333";
|
||||
el.style.boxShadow = "0 4px 6px rgba(0,0,0,0.1)";
|
||||
el.style.cursor = "pointer";
|
||||
el.style.userSelect = "none";
|
||||
el.style.transition = "transform 0.2s";
|
||||
el.innerText = node.text;
|
||||
if (node.raw._collapsed) {
|
||||
el.style.backgroundColor = "#f0f0f0";
|
||||
el.style.borderStyle = "dashed";
|
||||
}
|
||||
el.onmouseenter = () => el.style.transform = "scale(1.05)";
|
||||
el.onmouseleave = () => el.style.transform = "scale(1)";
|
||||
el.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
node.raw._collapsed = !node.raw._collapsed;
|
||||
this.render();
|
||||
};
|
||||
this.viewport.appendChild(el);
|
||||
});
|
||||
}
|
||||
addNode(parentId, text) {
|
||||
const findAndAdd = (node) => {
|
||||
if (node.id === parentId || node.text === parentId) {
|
||||
if (!node.children) node.children = [];
|
||||
node.children.push({ text, id: Math.random().toString(36).substr(2, 9) });
|
||||
return true;
|
||||
}
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
if (findAndAdd(child)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
findAndAdd(this.data);
|
||||
this.render();
|
||||
}
|
||||
removeNode(nodeId) {
|
||||
const remove = (node) => {
|
||||
if (!node.children) return false;
|
||||
const index = node.children.findIndex((child) => child.id === nodeId || child.text === nodeId);
|
||||
if (index !== -1) {
|
||||
node.children.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
for (const child of node.children) {
|
||||
if (remove(child)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (this.data.id === nodeId || this.data.text === nodeId) {
|
||||
console.warn("Cannot remove root node");
|
||||
return;
|
||||
}
|
||||
remove(this.data);
|
||||
this.render();
|
||||
}
|
||||
remove(this.data);
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
export {
|
||||
Mindmap
|
||||
};
|
||||
if (typeof globalThis !== "undefined" && globalThis.Component) {
|
||||
globalThis.Component.register("Mindmap", (container) => {
|
||||
let mm = null;
|
||||
const init = () => {
|
||||
container.innerHTML = "";
|
||||
mm = new Mindmap({
|
||||
container,
|
||||
data: container.state.data,
|
||||
nodeWidth: parseInt(container.getAttribute("node-width")) || 120,
|
||||
nodeHeight: parseInt(container.getAttribute("node-height")) || 40,
|
||||
hGap: parseInt(container.getAttribute("h-gap")) || 60,
|
||||
vGap: parseInt(container.getAttribute("v-gap")) || 20
|
||||
});
|
||||
container.mindmapInstance = mm;
|
||||
};
|
||||
container.state.__watch("data", (val) => mm ? (mm.data = val, mm.render()) : init());
|
||||
init();
|
||||
container.addEventListener("unload", () => mm = null);
|
||||
});
|
||||
}
|
||||
if (typeof globalThis !== "undefined") {
|
||||
globalThis.Mindmap = Mindmap;
|
||||
}
|
||||
exports2.Mindmap = Mindmap;
|
||||
Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
|
||||
});
|
||||
|
||||
2
dist/mindmap.min.js
vendored
2
dist/mindmap.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,16 +1,15 @@
|
||||
# mindmapTODO.md
|
||||
# mindmap TODO
|
||||
|
||||
## 当前版本
|
||||
- Tag: v1.0.0
|
||||
- 下一版本预期: v1.0.1
|
||||
- [x] 架构对齐:Align with `base` project's "No ESM" design.
|
||||
- [ ] 构建配置:Modify `vite.config.js` to output only UMD (`mindmap.js` and `mindmap.min.js`).
|
||||
- [ ] 源码重构:
|
||||
- [ ] 注册 `Mindmap` 为全局组件 (`globalThis.Component.register`).
|
||||
- [ ] 确保 `Mindmap` 类在 `globalThis` 上可用。
|
||||
- [ ] 测试验证:运行 `npm test` 确保功能无损。
|
||||
- [ ] 文档更新:
|
||||
- [ ] 更新 `package.json` 版本号。
|
||||
- [ ] 更新 `README.md` 以反映组件化用法。
|
||||
- [ ] 更新 `CHANGELOG.md`。
|
||||
|
||||
## 待办事项
|
||||
- [x] 实现树形数据的自动计算与布局。
|
||||
- [x] 支持节点的展开/折叠。
|
||||
- [x] 使用 SVG 绘制节点间的连接线。
|
||||
- [x] 优化大规模节点下的平移与缩放。
|
||||
- [x] 提供节点的动态增删 API。
|
||||
|
||||
## 重构建议
|
||||
- 考虑引入 `@web/state` 管理节点状态。
|
||||
- 使用 Canvas 或高级 SVG 技术优化超大规模树渲染。
|
||||
## 当前状态 (v1.0.1)
|
||||
- 1111 个节点渲染耗时: ~40ms (from TEST.md)
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@web/mindmap",
|
||||
"name": "@apigo.cc/mindmap",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@web/mindmap",
|
||||
"name": "@apigo.cc/mindmap",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
{
|
||||
"name": "@apigo.cc/mindmap",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"type": "module",
|
||||
"main": "dist/mindmap.js",
|
||||
"module": "dist/mindmap.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
|
||||
48
scripts/publish.js
Normal file
48
scripts/publish.js
Normal file
@ -0,0 +1,48 @@
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
try {
|
||||
// 1. 获取最新 tag
|
||||
let tag;
|
||||
try {
|
||||
tag = execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim();
|
||||
} catch (err) {
|
||||
throw new Error('Failed to find git tags. Please make sure the repository has tags (e.g., v1.0.0) before publishing.');
|
||||
}
|
||||
// 去掉 v 前缀
|
||||
const version = tag.startsWith('v') ? tag.slice(1) : tag;
|
||||
|
||||
console.log(`Latest git tag: ${tag}, Version to publish: ${version}`);
|
||||
|
||||
// 2. 读取并更新 package.json
|
||||
const pkgPath = path.join(__dirname, '../package.json');
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
|
||||
// 保持原有名称(如果已经带有 @apigo.cc/ 前缀)或替换前缀
|
||||
if (!pkg.name.startsWith('@apigo.cc/')) {
|
||||
const baseName = pkg.name.includes('/') ? pkg.name.split('/')[1] : pkg.name;
|
||||
pkg.name = `@apigo.cc/${baseName}`;
|
||||
}
|
||||
pkg.version = version;
|
||||
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
||||
console.log(`Updated package.json: name=${pkg.name}, version=${pkg.version}`);
|
||||
|
||||
// 3. 构建
|
||||
console.log('Running build...');
|
||||
execSync('npm run build', { stdio: 'inherit', cwd: path.join(__dirname, '..') });
|
||||
|
||||
// 4. 发布
|
||||
console.log('Publishing to npm...');
|
||||
const args = process.argv.slice(2).join(' ');
|
||||
execSync(`npm publish --access public ${args}`, { stdio: 'inherit', cwd: path.join(__dirname, '..') });
|
||||
|
||||
console.log('Publish successful!');
|
||||
} catch (error) {
|
||||
console.error('Publish failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
32
src/index.js
32
src/index.js
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @web/mindmap
|
||||
* @apigo.cc/mindmap
|
||||
* 类 Xmind 体验的思维导图引擎
|
||||
*/
|
||||
|
||||
@ -245,3 +245,33 @@ export class Mindmap {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
// 注册为全局组件
|
||||
if (typeof globalThis !== 'undefined' && globalThis.Component) {
|
||||
globalThis.Component.register('Mindmap', container => {
|
||||
let mm = null;
|
||||
const init = () => {
|
||||
container.innerHTML = '';
|
||||
mm = new Mindmap({
|
||||
container,
|
||||
data: container.state.data,
|
||||
nodeWidth: parseInt(container.getAttribute('node-width')) || 120,
|
||||
nodeHeight: parseInt(container.getAttribute('node-height')) || 40,
|
||||
hGap: parseInt(container.getAttribute('h-gap')) || 60,
|
||||
vGap: parseInt(container.getAttribute('v-gap')) || 20
|
||||
});
|
||||
container.mindmapInstance = mm;
|
||||
};
|
||||
|
||||
container.state.__watch('data', (val) => mm ? (mm.data = val, mm.render()) : init());
|
||||
|
||||
init();
|
||||
|
||||
container.addEventListener('unload', () => mm = null);
|
||||
});
|
||||
}
|
||||
|
||||
// 挂载到全局
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
globalThis.Mindmap = Mindmap;
|
||||
}
|
||||
|
||||
@ -10,9 +10,9 @@
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"@web/state": "../../state/src/index.js",
|
||||
"@web/base": "../../base/src/index.js",
|
||||
"@web/mindmap": "../src/index.js"
|
||||
"@apigo.cc/state": "../../state/src/index.js",
|
||||
"@apigo.cc/base": "../../base/src/index.js",
|
||||
"@apigo.cc/mindmap": "../src/index.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -22,7 +22,7 @@
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module">
|
||||
import { Mindmap } from '@web/mindmap';
|
||||
import { Mindmap } from '@apigo.cc/mindmap';
|
||||
window.testExports = { Mindmap };
|
||||
|
||||
const mm = new Mindmap({
|
||||
|
||||
@ -5,33 +5,37 @@ import terser from '@rollup/plugin-terser';
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@web/state': resolve(__dirname, '../state/src/index.js'),
|
||||
'@web/base': resolve(__dirname, '../base/src/index.js'),
|
||||
'@web/mindmap': resolve(__dirname, 'src/index.js')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
allow: ['..']
|
||||
'@apigo.cc/state': resolve(__dirname, '../state/src/index.js'),
|
||||
'@apigo.cc/base': resolve(__dirname, '../base/src/index.js'),
|
||||
'@apigo.cc/mindmap': resolve(__dirname, 'src/index.js')
|
||||
}
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.js'),
|
||||
name: 'Mindmap',
|
||||
formats: ['es']
|
||||
name: 'ApigoMindmap',
|
||||
formats: ['umd']
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['@web/state', '@web/base'],
|
||||
external: ['@apigo.cc/state', '@apigo.cc/base'],
|
||||
output: [
|
||||
{
|
||||
format: 'es',
|
||||
format: 'umd',
|
||||
name: 'ApigoMindmap',
|
||||
entryFileNames: 'mindmap.js',
|
||||
minifyInternalExports: false
|
||||
globals: {
|
||||
'@apigo.cc/state': 'ApigoState',
|
||||
'@apigo.cc/base': 'ApigoBase'
|
||||
}
|
||||
},
|
||||
{
|
||||
format: 'es',
|
||||
format: 'umd',
|
||||
name: 'ApigoMindmap',
|
||||
entryFileNames: 'mindmap.min.js',
|
||||
globals: {
|
||||
'@apigo.cc/state': 'ApigoState',
|
||||
'@apigo.cc/base': 'ApigoBase'
|
||||
},
|
||||
plugins: [terser()]
|
||||
}
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user