feat: align with base project's No ESM design and register global Mindmap component (by AI)

This commit is contained in:
AI Engineer 2026-06-12 02:01:57 +08:00
parent 85c2dd983d
commit 264911d495
13 changed files with 399 additions and 271 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
dist/
test-results/
.DS_Store

1
.npmrc Normal file
View File

@ -0,0 +1 @@
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}

View File

@ -1,5 +1,10 @@
# CHANGELOG
## v1.0.2 (2026-06-12)
- **架构对齐**: 遵循 `base` 包新设计,彻底消灭用户侧 ESM 强依赖。
- **组件注册**: 支持 `<Mindmap>` 全局组件声明式用法。
- **构建优化**: 产物调整为纯 UMD 模式,通过 `globalThis.Mindmap` 暴露 API。
## v1.0.0 (2026-05-29)
- 初始化项目架构。
- 实现基础渲染占位。

View File

@ -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()`

37
dist/mindmap.js vendored
View File

@ -1,4 +1,8 @@
class Mindmap {
(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: [] };
@ -199,7 +203,30 @@ class Mindmap {
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

File diff suppressed because one or more lines are too long

View File

@ -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
View File

@ -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",

View File

@ -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
View 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);
}

View File

@ -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;
}

View File

@ -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({

View File

@ -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()]
}
]