@qiulizhou/family-tree-editor

一个基于 Vue 3 的可配置家系图编辑器组件
1,286
Install
npm install @qiulizhou/family-tree-editor
Latest Version:1.0.8
Vue:3
License:MIT
Last Updated:Mar 10, 2026
Links: npm
Maintainer: qiulizhou

家系图编辑器 (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: 不显示删除线
默认图形

系统默认提供以下图形:

  1. 男性 - 蓝色方形
  2. 女性 - 粉色圆形
  3. 未知性别 - 灰色菱形
  4. 已故男性 - 蓝色方形 + 删除线
  5. 已故女性 - 粉色圆形 + 删除线

属性面板配置

属性面板配置文件位于 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')
默认字段

系统默认提供以下字段:

  1. 姓名 - 文本输入
  2. 年龄 - 文本输入
  3. 性别 - 下拉选择(男/女/未知)
  4. 状态 - 下拉选择(存活/已故)
  5. 备注 - 文本域

所有默认字段都可以通过配置进行修改、添加或移除。

发布检查清单

已完成的工作

✅ 组件封装
  • 创建 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.css
2. 本地测试
# 在当前项目中
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

注意事项

  1. 包名: 确保包名在 NPM 上是唯一的
  2. 版本号: 遵循语义化版本规范
  3. 依赖: 确保 peerDependencies 版本范围合理
  4. 文件大小: 检查打包后的文件大小
  5. 浏览器兼容性: 明确支持的浏览器版本
  6. 许可证: 选择合适的开源许可证

可选优化

  • TypeScript 支持:添加类型定义文件
  • 单元测试:使用 Vitest 编写测试
  • CI/CD:配置 GitHub Actions 自动化
  • 文档网站:使用 VitePress 创建文档站点

Related Packages

vue-family-tree-chart

Family tree chart Vue 3 component with WebWorker layout and SVG virtual render

1,520 0
@codemirror/lang-vue

Vue template support for the CodeMirror code editor

71,371,417 29
@gaoding/collage-editor

An editor to help user to create collage photos easily

4,989

Version History

Version Released Vue License
1.0.8 ^3.3.0 MIT
1.0.7 ^3.3.0 MIT
1.0.6 ^3.3.0 MIT
1.0.5 ^3.3.0 MIT
1.0.4 ^3.3.0 MIT