npm package: @hjxz/pharos-tree
Description: hjxz 树组件
Author: vino
Latest version: 0.0.1

# PharosTree 树组件

基于 Vue 3 和 Ant Design Vue 的树组件，支持无限级展开收起、拖拽、多选、联动等功能。

## 功能特性

- ✅ 可无限级展开收起子节点
- ✅ 可配置字符 fieldNames
- ✅ 树节点可以拖拽
- ✅ 支持静态数据和 API 请求两种数据导入方式
- ✅ 数据加载模式，加载中显示 Spinner 动画
- ✅ 可配置是否多选，支持全选、半选中、未选中状态
- ✅ 可配置联动规则，支持父子级节点是否联动
- ✅ 支持虚拟滚动，优化大量节点时的性能

## 安装

```bash
npm install @hjxz/pharos-tree
# 或
pnpm add @hjxz/pharos-tree
```

## 基础用法

### 静态数据

```vue
<template>
  <PharosTree :tree-data="treeData" />
</template>

<script setup lang="ts">
import { PharosTree } from '@hjxz/pharos-tree'

const treeData = [
  {
    key: '1',
    label: '节点 1',
    children: [
      {
        key: '1-1',
        label: '节点 1-1',
        children: [
          { key: '1-1-1', label: '节点 1-1-1' }
        ]
      }
    ]
  }
]
</script>
```

### API 数据

```vue
<template>
  <PharosTree :api="fetchTreeData" />
</template>

<script setup lang="ts">
import { PharosTree } from '@hjxz/pharos-tree'

const fetchTreeData = async (params?: { parentId?: string }) => {
  // 请求 API
  const response = await fetch(`/api/tree?parentId=${params?.parentId || ''}`)
  return response.json()
}
</script>
```

### 配置 fieldNames

```vue
<template>
  <PharosTree 
    :tree-data="treeData"
    :field-names="{
      key: 'id',
      label: 'name',
      children: 'childList'
    }"
  />
</template>
```

### 多选模式

```vue
<template>
  <PharosTree 
    :tree-data="treeData"
    checkable
    :check-strictly="false"
    v-model:checked-keys="checkedKeys"
    @check="handleCheck"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'

const checkedKeys = ref<string[]>([])

const handleCheck = (keys: string[], info: any) => {
  console.log('选中的节点:', keys)
  console.log('半选中的节点:', info.halfCheckedKeys)
}
</script>
```

### 拖拽功能

```vue
<template>
  <PharosTree 
    :tree-data="treeData"
    draggable
    @dragstart="handleDragStart"
    @drop="handleDrop"
  />
</template>

<script setup lang="ts">
const handleDragStart = (info: any) => {
  console.log('开始拖拽:', info.node)
}

const handleDrop = (info: any) => {
  console.log('放置节点:', info.node)
  console.log('拖拽的节点:', info.dragNode)
  // 在这里处理节点移动逻辑
}
</script>
```

### 虚拟滚动

```vue
<template>
  <PharosTree 
    :tree-data="treeData"
    :virtual-scroll="true"
    :visible-count="15"
    :list-height="400"
    :item-height="32"
  />
</template>

<script setup lang="ts">
import { PharosTree } from '@hjxz/pharos-tree'

// 虚拟滚动适用于节点数量较多的场景，可以显著提升性能
// visibleCount: 控制在视口中同时渲染的节点数量
// listHeight: 设置树容器的固定高度，超过此高度时显示滚动条
// itemHeight: 每个节点的高度，需要与实际节点高度一致
</script>
```

## API

### Props

| 参数 | 说明 | 类型 | 默认值 |
|------|------|------|--------|
| treeData | 树形数据 | `TreeNode[]` | `[]` |
| api | 异步获取树数据的函数 | `(params?: { parentId?: string }) => Promise<TreeNode[]>` | - |
| indent | 缩进距离（px） | `number` | `16` |
| disabled | 是否禁用 | `boolean` | `false` |
| checkable | 是否显示复选框 | `boolean` | `false` |
| multiple | 是否多选 | `boolean` | `false` |
| fieldNames | 字段映射配置 | `TreeFieldNames` | - |
| draggable | 是否可拖拽 | `boolean` | `false` |
| checkStrictly | 是否启用父子节点关联。false 表示父子节点关联，true 表示不关联 | `boolean` | `false` |
| defaultExpandedKeys | 默认展开的节点 key | `string[]` | `[]` |
| defaultCheckedKeys | 默认选中的节点 key | `string[]` | `[]` |
| expandedKeys | 受控的展开节点 key（v-model:expandedKeys） | `string[]` | - |
| checkedKeys | 受控的选中节点 key（v-model:checkedKeys） | `string[]` | - |
| itemHeight | 节点高度（px） | `number` | `32` |
| noDataText | 无数据提示文本 | `string` | `'暂无数据'` |
| virtualScroll | 是否启用虚拟滚动 | `boolean` | `false` |
| visibleCount | 可见节点数量（仅在启用虚拟滚动时有效） | `number` | `10` |
| listHeight | 列表高度（px，仅在启用虚拟滚动时有效） | `number` | `256` |

### Events

| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| update:checkedKeys | 选中节点变化时触发 | `(keys: string[])` |
| update:expandedKeys | 展开节点变化时触发 | `(keys: string[])` |
| check | 点击复选框时触发 | `(keys: string[], info: { checked: boolean; node: TreeNodeInternal; checkedNodes: TreeNodeInternal[]; halfCheckedKeys?: string[] })` |
| expand | 展开/收起节点时触发 | `(keys: string[], info: { expanded: boolean; node: TreeNodeInternal })` |
| select | 选择节点时触发（单选模式） | `(keys: string[], info: { node: TreeNodeInternal; selected: boolean })` |
| dragstart | 开始拖拽时触发 | `(info: { event: DragEvent; node: TreeNodeInternal })` |
| dragenter | 拖拽进入节点时触发 | `(info: { event: DragEvent; node: TreeNodeInternal; expanded: boolean })` |
| dragover | 拖拽悬停在节点上时触发 | `(info: { event: DragEvent; node: TreeNodeInternal })` |
| dragleave | 拖拽离开节点时触发 | `(info: { event: DragEvent; node: TreeNodeInternal })` |
| drop | 放置节点时触发 | `(info: { event: DragEvent; node: TreeNodeInternal; dragNode: TreeNodeInternal })` |
| dragend | 拖拽结束时触发 | `(info: { event: DragEvent; node: TreeNodeInternal })` |
| load | 节点懒加载完成时触发 | `(node: TreeNodeInternal)` |

### TreeFieldNames

| 字段 | 说明 | 类型 | 默认值 |
|------|------|------|--------|
| key | 唯一标识字段名 | `string` | `'key'` |
| label | 显示文本字段名 | `string` | `'label'` |
| value | 值字段名 | `string` | `'key'` |
| children | 子节点字段名 | `string` | `'children'` |
| disabled | 禁用状态字段名 | `string` | `'disabled'` |
| isLeaf | 是否叶子节点字段名 | `string` | `'isLeaf'` |
| extra | 额外数据字段名 | `string` | `'extra'` |

### TreeNode

| 字段 | 说明 | 类型 | 必填 |
|------|------|------|------|
| key | 唯一标识 | `string` | ✅ |
| label | 显示文本 | `string` | ✅ |
| children | 子节点 | `TreeNode[]` | - |
| disabled | 是否禁用 | `boolean` | - |
| isLeaf | 是否叶子节点 | `boolean` | - |
| value | 值 | `string` | - |
| extra | 额外数据 | `any` | - |

## 示例

### 完整示例

```vue
<template>
  <div>
    <PharosTree
      :tree-data="treeData"
      :field-names="fieldNames"
      checkable
      :check-strictly="false"
      draggable
      :virtual-scroll="true"
      :visible-count="15"
      :list-height="400"
      :default-expanded-keys="['1']"
      :default-checked-keys="['1-1']"
      v-model:checked-keys="checkedKeys"
      v-model:expanded-keys="expandedKeys"
      @check="handleCheck"
      @expand="handleExpand"
      @drop="handleDrop"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { PharosTree } from '@hjxz/pharos-tree'

const treeData = [
  {
    key: '1',
    label: '父节点 1',
    children: [
      { key: '1-1', label: '子节点 1-1' },
      { key: '1-2', label: '子节点 1-2' }
    ]
  },
  {
    key: '2',
    label: '父节点 2',
    children: [
      { key: '2-1', label: '子节点 2-1' },
      { key: '2-2', label: '子节点 2-2' }
    ]
  }
]

const fieldNames = {
  key: 'key',
  label: 'label',
  children: 'children'
}

const checkedKeys = ref<string[]>([])
const expandedKeys = ref<string[]>([])

const handleCheck = (keys: string[], info: any) => {
  console.log('选中的节点:', keys)
  console.log('半选中的节点:', info.halfCheckedKeys)
}

const handleExpand = (keys: string[], info: any) => {
  console.log('展开的节点:', keys)
}

const handleDrop = (info: any) => {
  console.log('放置节点:', info.node)
  console.log('拖拽的节点:', info.dragNode)
}
</script>
```

## 注意事项

1. 当使用 API 方式加载数据时，组件会在节点展开时自动调用 API 加载子节点（懒加载）
2. 拖拽功能中，不允许将节点拖拽到自己的子节点中
3. 半选中状态只在 `checkStrictly: false`（父子节点关联）时生效
4. 当 `checkStrictly: true` 时，父子节点的选中状态互不影响
5. **虚拟滚动**：
   - 当节点数量较多（建议超过 100 个可见节点）时，建议启用虚拟滚动以提升性能
   - `itemHeight` 需要与实际节点高度一致，否则可能出现滚动位置不准确的问题
   - `visibleCount` 建议设置为略大于可视区域内能显示的节点数量，一般设置为 `Math.ceil(listHeight / itemHeight) + 2`
   - 虚拟滚动模式下，只渲染可见区域的节点，滚动时会动态加载和卸载节点
   - 虚拟滚动与拖拽功能完全兼容，可以同时使用

## License

MIT