@qiulizhou/family-tree-editor
v1.0.8
Published
一个基于 Vue 3 的可配置家系图编辑器组件
Maintainers
Readme
家系图编辑器 (Family Tree Editor)
一个基于 Vue 3 的可配置家系图编辑器组件,支持拖拽创建节点、编辑关系、自定义样式等功能。
特性
- 🎨 可视化编辑家系图
- 🔧 高度可配置(颜色、文本、功能开关)
- 📱 响应式设计
- 🎯 支持多种节点类型(男性、女性、未知性别)
- 🔗 支持多种关系类型(婚姻、亲子、兄弟姐妹等)
- 💾 支持数据导入导出
- 🌈 自动适配深色/浅色主题
- 🎭 支持 SCSS 变量集成
安装
npm install @qiulizhou/family-tree-editor
# 或
pnpm add @qiulizhou/family-tree-editor快速开始
<template>
<FamilyTreeEditor
:initial-data="treeData"
:config="config"
@save="handleSave"
@change="handleChange"
/>
</template>
<script setup>
import { ref } from 'vue'
import FamilyTreeEditor from '@qiulizhou/family-tree-editor'
import '@qiulizhou/family-tree-editor/dist/style.css'
const treeData = ref({
nodes: [],
edges: [],
canvasSettings: {}
})
const config = ref({
colors: {
primary: '#1890ff'
},
i18n: {
title: '我的家系图'
}
})
const handleSave = async (data) => {
// 调用后端 API 保存
await api.saveFamilyTree(data)
}
const handleChange = (data) => {
console.log('数据已变化', data)
}
</script>API 文档
Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| initialData | Object | { nodes: [], edges: [], canvasSettings: {} } | 初始化数据 |
| config | Object | {} | 配置项 |
| readonly | Boolean | false | 是否只读模式 |
| height | String | '100vh' | 编辑器高度 |
Events
| 事件名 | 参数 | 说明 |
|--------|------|------|
| save | (data: Object) | 保存事件 |
| change | (data: Object) | 数据变化事件 |
| node-select | (node: Object) | 节点选择事件 |
| node-add | (node: Object) | 节点添加事件 |
| node-delete | (nodeId: String) | 节点删除事件 |
| node-update | (node: Object) | 节点更新事件 |
方法(通过 ref 调用)
| 方法名 | 参数 | 返回值 | 说明 | |--------|------|--------|------| | exportData | - | Object | 导出当前数据 | | importData | data: Object | - | 导入数据 | | resetData | - | - | 重置数据 |
配置项详解
完整配置示例
const config = {
// 颜色配置
colors: {
primary: '#1890ff', // 主题色
unsavedBadge: '#faad14', // 未保存标记颜色
sick: '#faad14', // 患病状态颜色
alive: '#52c41a', // 存活状态颜色
deceased: '#999', // 已故状态颜色
warning: '#ffa940', // 警告颜色
danger: '#ff4d4f' // 危险颜色
},
// 文本国际化配置
i18n: {
title: '家系图编辑器',
unsaved: '未保存',
save: '保存',
clear: '清空画布',
undo: '撤销',
redo: '重做',
clearConfirm: '确定要清空画布吗?此操作不可恢复!',
deleteConfirm: '确定要删除该节点吗?',
propertyPanel: '属性编辑',
emptyText: '请选择一个节点进行编辑',
deleteButton: '删除节点',
toolbar: '图形工具',
baseSettings: '基础配置',
fontSize: '字体大小',
lineStyle: '线条样式',
lineWidth: '线条粗细',
lineEnd: '线条端点'
},
// 功能开关
features: {
showToolbar: true, // 显示工具栏
showPropertyPanel: true, // 显示属性面板
showActionBar: true, // 显示操作栏
showUnsavedBadge: true, // 显示未保存标记
enableUndo: false, // 启用撤销
enableRedo: false, // 启用重做
enableSave: true, // 启用保存
enableClear: true, // 启用清空
enableDelete: true // 启用删除
},
// 画布设置
canvas: {
defaultZoom: 1, // 默认缩放
minZoom: 0.5, // 最小缩放
maxZoom: 2, // 最大缩放
gridGap: 20 // 网格间距
},
// 节点默认设置
node: {
defaultName: '未命名', // 默认名称
defaultStatus: 'alive', // 默认状态
fontSize: 12, // 字体大小
spacing: 150 // 节点间距
},
// 按钮样式配置
buttons: {
save: {
type: 'primary', // 按钮类型: primary | success | warning | danger | info | default
plain: false, // 是否朴素按钮
round: false, // 是否圆角按钮
circle: false // 是否圆形按钮
},
clear: {
type: 'warning',
plain: false,
round: false,
circle: false
},
undo: {
type: 'default',
plain: false,
round: false,
circle: false
},
redo: {
type: 'default',
plain: false,
round: false,
circle: false
}
}
}数据格式
节点数据结构
{
id: 'node-1', // 节点ID(必需)
type: 'male', // 节点类型: 'male' | 'female' | 'unknown'
position: { x: 400, y: 300 }, // 节点位置
data: {
name: '张三', // 姓名
gender: 'male', // 性别
nodeType: 'male', // ⚠️ 重要:必须设置,用于显示正确的图形
birthDate: '1990-01-01', // 出生日期
relationship: 'self', // 关系类型
status: 'alive', // 状态: 'alive' | 'sick' | 'deceased'
diseaseName: '高血压', // 疾病名称(status为sick时)
note: '备注信息' // 备注
}
}完整数据示例
{
"version": "1.0.0",
"nodes": [
{
"id": "proband-1",
"type": "female",
"position": { "x": 400, "y": 300 },
"data": {
"name": "李娥",
"gender": "female",
"nodeType": "female",
"relationship": "self",
"status": "alive"
}
}
],
"edges": [],
"canvasSettings": {
"fontSize": 12,
"edgeType": "default",
"edgeStrokeWidth": 2,
"markerEnd": "arrow"
}
}使用示例
从后端加载数据
<script setup>
import { ref, onMounted } from 'vue'
const treeData = ref({
nodes: [],
edges: [],
canvasSettings: {}
})
onMounted(async () => {
const response = await fetch('/api/family-tree/123')
const data = await response.json()
treeData.value = data
})
const handleSave = async (data) => {
await fetch('/api/family-tree/123', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
alert('保存成功')
}
</script>使用 ref 调用方法
<template>
<div>
<button @click="exportData">导出数据</button>
<button @click="importData">导入数据</button>
<button @click="resetData">重置数据</button>
<FamilyTreeEditor
ref="editorRef"
:initial-data="treeData"
:config="config"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
const editorRef = ref(null)
const exportData = () => {
const data = editorRef.value.exportData()
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'family-tree.json'
a.click()
}
const importData = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json'
input.onchange = (e) => {
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = (event) => {
const data = JSON.parse(event.target.result)
editorRef.value.importData(data)
}
reader.readAsText(file)
}
input.click()
}
const resetData = () => {
if (confirm('确定要重置所有数据吗?')) {
editorRef.value.resetData()
}
}
</script>只读模式
<template>
<FamilyTreeEditor
:initial-data="treeData"
:readonly="true"
:config="{ features: { showActionBar: false, showToolbar: false } }"
/>
</template>自定义高度
<template>
<FamilyTreeEditor
:initial-data="treeData"
height="600px"
/>
</template>主题定制
方法1: 通过配置修改
<FamilyTreeEditor
:config="{
colors: {
primary: '#2c3e50' // 深灰色
}
}"
:initial-data="treeData"
/>方法2: 使用 SCSS 变量(推荐)
如果你的项目使用 SCSS 主题变量:
<template>
<FamilyTreeEditor
:config="editorConfig"
:initial-data="treeData"
/>
</template>
<script setup>
import { computed } from 'vue'
// 读取 CSS 变量
const getPrimaryColor = () => {
return getComputedStyle(document.documentElement)
.getPropertyValue('--primary-color')
.trim() || '#1890ff'
}
const editorConfig = computed(() => ({
colors: {
primary: getPrimaryColor()
}
}))
</script>方法3: 直接使用 CSS 变量覆盖
在你的项目 CSS 中:
// 你的主题变量
$primary-color: #2c3e50;
:root {
--primary-color: #{$primary-color};
}
// 覆盖组件的 CSS 变量
.family-tree-editor {
--editor-primary-color: var(--primary-color);
}注意: 组件会自动检测背景色的亮度,深色背景显示白色文字,浅色背景显示深色文字。
按钮样式配置
你可以自定义操作栏按钮的样式:
<FamilyTreeEditor
:config="{
buttons: {
save: {
type: 'success', // 改为绿色成功按钮
plain: false,
round: true // 圆角按钮
},
clear: {
type: 'danger', // 改为红色危险按钮
plain: true // 朴素按钮
},
undo: {
type: 'info', // 信息按钮
plain: false
},
redo: {
type: 'info',
plain: false
}
}
}"
:initial-data="treeData"
/>按钮类型 (type)
primary: 主要按钮(蓝色)success: 成功按钮(绿色)warning: 警告按钮(橙色)danger: 危险按钮(红色)info: 信息按钮(灰色)default: 默认按钮(白色)
按钮属性
plain: 朴素按钮(镂空样式)round: 圆角按钮circle: 圆形按钮(仅显示图标)
自定义按钮
你可以通过插槽添加自定义按钮到操作栏:
<template>
<FamilyTreeEditor
:config="{
features: {
enableCustomButtons: true // 启用自定义按钮插槽
}
}"
:initial-data="treeData"
>
<template #custom-buttons="{ store }">
<!-- 添加导出按钮 -->
<el-button type="success" @click="handleExport(store)">
<el-icon><Download /></el-icon>
导出
</el-button>
<!-- 添加打印按钮 -->
<el-button type="info" @click="handlePrint">
<el-icon><Printer /></el-icon>
打印
</el-button>
<!-- 添加分享按钮 -->
<el-button type="primary" plain @click="handleShare">
<el-icon><Share /></el-icon>
分享
</el-button>
</template>
</FamilyTreeEditor>
</template>
<script setup>
import { Download, Printer, Share } from '@element-plus/icons-vue'
const handleExport = (store) => {
// 访问 store 获取数据
const data = {
nodes: store.nodes,
edges: store.edges
}
// 导出为 JSON
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'family-tree.json'
a.click()
}
const handlePrint = () => {
window.print()
}
const handleShare = () => {
// 实现分享逻辑
console.log('分享家系图')
}
</script>插槽参数
store: Pinia store 实例,可以访问nodes、edges、canvasSettings等数据
常见问题
1. 性别图形显示错误
问题: 明明是女性,但显示为菱形(未知性别)
原因: 缺少 data.nodeType 字段
解决方案:
// ❌ 错误
const gender = info.gender?.includes('女') ? 'female' : 'male'
treeData.value = {
nodes: [{
type: gender,
data: {
gender // 缺少 nodeType
}
}]
}
// ✅ 正确
const gender = info.gender?.includes('女') ? 'female' : 'male'
treeData.value = {
nodes: [{
type: gender,
data: {
gender,
nodeType: gender // 必须添加
}
}]
}2. CSS 样式导入问题
问题: 在另一个项目中引用时,CSS 文件找不到
原因: package.json 的 exports 配置不正确
解决方案: 确保正确导入 CSS
import '@qiulizhou/family-tree-editor/dist/style.css'3. "未保存"标记不消失
问题: 点击保存后,"未保存"标记仍然显示
解决方案: 组件已自动处理,确保使用最新版本(v1.0.1+)
发布到 NPM
构建和发布
# 1. 构建库文件
pnpm build:lib
# 2. 更新版本号
npm version patch # 1.0.0 -> 1.0.1
npm version minor # 1.0.0 -> 1.1.0
npm version major # 1.0.0 -> 2.0.0
# 3. 发布到 NPM
npm publish --access public本地测试
# 在家系图项目中
pnpm build:lib
npm link
# 在测试项目中
npm link @qiulizhou/family-tree-editor
# 测试完成后取消链接
npm unlink @qiulizhou/family-tree-editor开发
# 安装依赖
pnpm install
# 开发模式
pnpm dev
# 构建库
pnpm build:lib
# 预览
pnpm preview技术栈
- Vue 3
- Vite
- Element Plus
- Vue Flow
- Pinia
License
MIT
作者
qiulizhou
贡献
欢迎提交 Issue 和 Pull Request!
高级配置
工具栏配置
工具栏配置文件位于 src/config/toolbarConfig.js
修改工具栏标题
import { updateToolbarTitle } from '@/config/toolbarConfig'
updateToolbarTitle('我的图形工具')添加自定义图形
import { addCustomShape } from '@/config/toolbarConfig'
addCustomShape({
type: 'adopted', // 唯一标识
label: '领养', // 显示名称
icon: {
shape: 'square', // 形状: square(方形), circle(圆形), diamond(菱形)
color: '#e8f5e9', // 背景颜色
borderColor: '#4caf50', // 边框颜色
strikethrough: false // 是否显示删除线
}
})移除图形
import { removeShape } from '@/config/toolbarConfig'
removeShape('unknown') // 移除未知性别图形图形配置参数
shape (形状)
square: 方形circle: 圆形diamond: 菱形(会自动旋转45度)
color (背景颜色)
- 支持任何 CSS 颜色值:十六进制、RGB、颜色名
borderColor (边框颜色)
- 同 color,支持任何 CSS 颜色值
strikethrough (删除线)
true: 显示删除线(用于表示已故)false: 不显示删除线
默认图形
系统默认提供以下图形:
- 男性 - 蓝色方形
- 女性 - 粉色圆形
- 未知性别 - 灰色菱形
- 已故男性 - 蓝色方形 + 删除线
- 已故女性 - 粉色圆形 + 删除线
属性面板配置
属性面板配置文件位于 src/config/propertyPanelConfig.js
修改面板标题和空状态文本
import { updatePanelTitle, updateEmptyText } from '@/config/propertyPanelConfig'
updatePanelTitle('成员信息')
updateEmptyText('请选择家族成员查看详情')字段类型
1. 输入框 (input)
{
key: 'name',
label: '姓名',
type: 'input',
placeholder: '请输入姓名',
inputType: 'text', // text, number, email, tel, date, time 等
required: true
}2. 下拉选择 (select)
{
key: 'gender',
label: '性别',
type: 'select',
options: [
{ label: '男', value: '男' },
{ label: '女', value: '女' }
]
}3. 文本域 (textarea)
{
key: 'note',
label: '备注',
type: 'textarea',
placeholder: '请输入备注信息',
rows: 4
}添加自定义字段
import { addCustomField } from '@/config/propertyPanelConfig'
// 添加出生日期
addCustomField({
key: 'birthDate',
label: '出生日期',
type: 'input',
inputType: 'date'
})
// 添加职业
addCustomField({
key: 'occupation',
label: '职业',
type: 'input',
placeholder: '请输入职业'
})
// 添加婚姻状态
addCustomField({
key: 'maritalStatus',
label: '婚姻状态',
type: 'select',
options: [
{ label: '未婚', value: '未婚' },
{ label: '已婚', value: '已婚' },
{ label: '离异', value: '离异' },
{ label: '丧偶', value: '丧偶' }
]
})管理选择框选项
import {
updateSelectOptions,
addSelectOption,
removeSelectOption
} from '@/config/propertyPanelConfig'
// 更新所有选项
updateSelectOptions('gender', [
{ label: '男性', value: '男' },
{ label: '女性', value: '女' },
{ label: '其他', value: '其他' }
])
// 添加单个选项
addSelectOption('status', { label: '失踪', value: '失踪' })
// 移除单个选项
removeSelectOption('gender', '未知')字段管理
import {
removeField,
updateField,
configureFields
} from '@/config/propertyPanelConfig'
// 移除字段
removeField('age')
// 更新字段
updateField('name', {
placeholder: '请输入完整姓名'
})
// 批量配置所有字段
configureFields([
{
key: 'fullName',
label: '全名',
type: 'input',
placeholder: '请输入全名'
},
{
key: 'birthYear',
label: '出生年份',
type: 'input',
inputType: 'number'
}
])完整配置示例
在 src/main.js 中:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
// 导入配置方法
import {
addCustomField,
updatePanelTitle,
updateSelectOptions,
addSelectOption
} from '@/config/propertyPanelConfig'
import {
addCustomShape,
updateToolbarTitle
} from '@/config/toolbarConfig'
// 配置属性面板
updatePanelTitle('成员信息')
updateSelectOptions('gender', [
{ label: '男性', value: '男' },
{ label: '女性', value: '女' }
])
addCustomField({
key: 'birthDate',
label: '出生日期',
type: 'input',
inputType: 'date'
})
addCustomField({
key: 'occupation',
label: '职业',
type: 'input',
placeholder: '请输入职业'
})
// 配置工具栏
updateToolbarTitle('家族关系图')
addCustomShape({
type: 'adopted',
label: '领养',
icon: {
shape: 'square',
color: '#e8f5e9',
borderColor: '#4caf50'
}
})
const app = createApp(App)
app.use(createPinia())
app.use(ElementPlus)
app.mount('#app')默认字段
系统默认提供以下字段:
- 姓名 - 文本输入
- 年龄 - 文本输入
- 性别 - 下拉选择(男/女/未知)
- 状态 - 下拉选择(存活/已故)
- 备注 - 文本域
所有默认字段都可以通过配置进行修改、添加或移除。
发布检查清单
已完成的工作
✅ 组件封装
- 创建 FamilyTreeEditor 主组件
- 支持 props: initialData, config, readonly, height
- 支持 events: save, change, node-select
- 暴露方法: exportData(), importData(), resetData()
✅ 配置系统
- 创建 defaultConfig.js 默认配置
- 支持颜色、文本、功能开关、画布、节点配置
- 实现 mergeConfig 深度合并函数
✅ 数据管理
- 移除 localStorage 依赖
- 通过 props 传入初始数据
- 通过 events 通知外部保存
- 标准化 JSON 数据格式
✅ 打包配置
- 配置 package.json (name, version, main, module, exports)
- 配置 peerDependencies
- 配置 vite.config.js 库模式
- 创建入口文件和全局样式
发布前检查
1. 测试构建
# 构建库
pnpm build:lib
# 检查 dist 目录
ls dist/
# 应该包含:
# - family-tree-editor.es.js
# - family-tree-editor.umd.js
# - family-tree-editor.css2. 本地测试
# 在当前项目中
pnpm build:lib
npm link
# 在测试项目中
npm link @qiulizhou/family-tree-editor
# 测试导入
import FamilyTreeEditor from '@qiulizhou/family-tree-editor'
import '@qiulizhou/family-tree-editor/dist/style.css'3. 更新 package.json
确保以下字段正确:
name: 包名version: 版本号description: 描述keywords: 关键词author: 作者license: 许可证repository: 仓库地址homepage: 主页
4. 发布到 NPM
# 登录 NPM
npm login
# 发布(首次)
npm publish --access public
# 更新版本并发布
npm version patch # 1.0.0 -> 1.0.1
npm version minor # 1.0.0 -> 1.1.0
npm version major # 1.0.0 -> 2.0.0
npm publish注意事项
- 包名: 确保包名在 NPM 上是唯一的
- 版本号: 遵循语义化版本规范
- 依赖: 确保 peerDependencies 版本范围合理
- 文件大小: 检查打包后的文件大小
- 浏览器兼容性: 明确支持的浏览器版本
- 许可证: 选择合适的开源许可证
可选优化
- TypeScript 支持:添加类型定义文件
- 单元测试:使用 Vitest 编写测试
- CI/CD:配置 GitHub Actions 自动化
- 文档网站:使用 VitePress 创建文档站点
