npm package: @qiulizhou/family-tree-editor
Description: 一个基于 Vue 3 的可配置家系图编辑器组件
Author: qiulizhou
Latest version: 1.0.8

# 家系图编辑器 (Family Tree Editor)

一个基于 Vue 3 的可配置家系图编辑器组件，支持拖拽创建节点、编辑关系、自定义样式等功能。

## 特性

- 🎨 可视化编辑家系图
- 🔧 高度可配置（颜色、文本、功能开关）
- 📱 响应式设计
- 🎯 支持多种节点类型（男性、女性、未知性别）
- 🔗 支持多种关系类型（婚姻、亲子、兄弟姐妹等）
- 💾 支持数据导入导出
- 🌈 自动适配深色/浅色主题
- 🎭 支持 SCSS 变量集成

## 安装

```bash
npm install @qiulizhou/family-tree-editor
# 或
pnpm add @qiulizhou/family-tree-editor
```

## 快速开始

```vue
<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 | - | - | 重置数据 |

## 配置项详解

### 完整配置示例

```javascript
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
    }
  }
}
```

## 数据格式

### 节点数据结构

```javascript
{
  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: '备注信息'         // 备注
  }
}
```

### 完整数据示例

```json
{
  "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"
  }
}
```

## 使用示例

### 从后端加载数据

```vue
<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 调用方法

```vue
<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>
```

### 只读模式

```vue
<template>
  <FamilyTreeEditor
    :initial-data="treeData"
    :readonly="true"
    :config="{ features: { showActionBar: false, showToolbar: false } }"
  />
</template>
```

### 自定义高度

```vue
<template>
  <FamilyTreeEditor
    :initial-data="treeData"
    height="600px"
  />
</template>
```

## 主题定制

### 方法1: 通过配置修改

```vue
<FamilyTreeEditor
  :config="{
    colors: {
      primary: '#2c3e50'  // 深灰色
    }
  }"
  :initial-data="treeData"
/>
```

### 方法2: 使用 SCSS 变量（推荐）

如果你的项目使用 SCSS 主题变量：

```vue
<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 中：

```scss
// 你的主题变量
$primary-color: #2c3e50;

:root {
  --primary-color: #{$primary-color};
}

// 覆盖组件的 CSS 变量
.family-tree-editor {
  --editor-primary-color: var(--primary-color);
}
```

**注意**: 组件会自动检测背景色的亮度，深色背景显示白色文字，浅色背景显示深色文字。

### 按钮样式配置

你可以自定义操作栏按钮的样式：

```vue
<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`: 圆形按钮（仅显示图标）

### 自定义按钮

你可以通过插槽添加自定义按钮到操作栏：

```vue
<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` 字段

**解决方案**:

```javascript
// ❌ 错误
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

```javascript
import '@qiulizhou/family-tree-editor/dist/style.css'
```

### 3. "未保存"标记不消失

**问题**: 点击保存后，"未保存"标记仍然显示

**解决方案**: 组件已自动处理，确保使用最新版本（v1.0.1+）

## 发布到 NPM

### 构建和发布

```bash
# 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
```

### 本地测试

```bash
# 在家系图项目中
pnpm build:lib
npm link

# 在测试项目中
npm link @qiulizhou/family-tree-editor

# 测试完成后取消链接
npm unlink @qiulizhou/family-tree-editor
```

## 开发

```bash
# 安装依赖
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`

#### 修改工具栏标题

```javascript
import { updateToolbarTitle } from '@/config/toolbarConfig'

updateToolbarTitle('我的图形工具')
```

#### 添加自定义图形

```javascript
import { addCustomShape } from '@/config/toolbarConfig'

addCustomShape({
  type: 'adopted',        // 唯一标识
  label: '领养',          // 显示名称
  icon: {
    shape: 'square',      // 形状: square(方形), circle(圆形), diamond(菱形)
    color: '#e8f5e9',     // 背景颜色
    borderColor: '#4caf50', // 边框颜色
    strikethrough: false  // 是否显示删除线
  }
})
```

#### 移除图形

```javascript
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`

#### 修改面板标题和空状态文本

```javascript
import { updatePanelTitle, updateEmptyText } from '@/config/propertyPanelConfig'

updatePanelTitle('成员信息')
updateEmptyText('请选择家族成员查看详情')
```

#### 字段类型

**1. 输入框 (input)**

```javascript
{
  key: 'name',
  label: '姓名',
  type: 'input',
  placeholder: '请输入姓名',
  inputType: 'text',  // text, number, email, tel, date, time 等
  required: true
}
```

**2. 下拉选择 (select)**

```javascript
{
  key: 'gender',
  label: '性别',
  type: 'select',
  options: [
    { label: '男', value: '男' },
    { label: '女', value: '女' }
  ]
}
```

**3. 文本域 (textarea)**

```javascript
{
  key: 'note',
  label: '备注',
  type: 'textarea',
  placeholder: '请输入备注信息',
  rows: 4
}
```

#### 添加自定义字段

```javascript
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: '丧偶' }
  ]
})
```

#### 管理选择框选项

```javascript
import {
  updateSelectOptions,
  addSelectOption,
  removeSelectOption
} from '@/config/propertyPanelConfig'

// 更新所有选项
updateSelectOptions('gender', [
  { label: '男性', value: '男' },
  { label: '女性', value: '女' },
  { label: '其他', value: '其他' }
])

// 添加单个选项
addSelectOption('status', { label: '失踪', value: '失踪' })

// 移除单个选项
removeSelectOption('gender', '未知')
```

#### 字段管理

```javascript
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` 中：

```javascript
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. 测试构建

```bash
# 构建库
pnpm build:lib

# 检查 dist 目录
ls dist/
# 应该包含:
# - family-tree-editor.es.js
# - family-tree-editor.umd.js
# - family-tree-editor.css
```

#### 2. 本地测试

```bash
# 在当前项目中
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

```bash
# 登录 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 创建文档站点