# 树视图组件

一个用于展示嵌套数据结构的层级树视图组件。支持可展开/折叠的节点、延迟加载、上下文菜单和自定义节点显示。非常适合文件浏览器、组织结构图和层级数据导航。

## 安装

```bash
npm install @ticatec/uniface-element
```

## 导入

```typescript
import TreeView, { type LazyLoader, type NodeVisibleFun, type OnNodeSelectionChange } from "@ticatec/uniface-element/TreeView";
import TreeNodes, { type TreeNode } from "@ticatec/uniface-element/TreeNodes";
```

## 基本用法

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let activeNode = null;
  
  // 示例数据
  const data = [
    { id: 1, name: "文档", parent: null },
    { id: 2, name: "项目", parent: null },
    { id: 3, name: "项目 A", parent: 2 },
    { id: 4, name: "项目 B", parent: 2 },
    { id: 5, name: "文件1.txt", parent: 3 },
    { id: 6, name: "文件2.txt", parent: 3 }
  ];
  
  // 创建树结构
  const treeNodes = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => item.parent === null,
    checkIsDirectory: (node) => node.children != null
  });
  
  treeNodes.setData(data);
  
  function handleSelectionChange(node) {
    console.log('选中节点：', node);
    return true; // 允许选择
  }
</script>

<TreeView
  nodes={treeNodes.nodes}
  version={treeNodes.version}
  textField="name"
  bind:activeNode
  onchange={handleSelectionChange}
  checkIsDirectory={node => node.children != null}
/>
```

## 属性

| 属性 | 类型 | 默认值 | 描述 |
|------|------|---------|-------------|
| `nodes` | `Array<TreeNode<any>>` | 必需 | 要显示的树节点数组 |
| `textField` | `string \| GetText<any>` | 必需 | 节点文本的字段名称或获取文本的函数 |
| `activeNode` | `any` | `null` | 当前选中的/活动节点 |
| `style` | `string` | `""` | 附加 CSS 样式 |
| `title` | `string` | `undefined` | 可选的标题，显示在树视图顶部（固定位置，不滚动） |
| `lazyLoader` | `LazyLoader \| null` | `null` | 延迟加载配置 |
| `isVisible` | `NodeVisibleFun` | `null` | 判断节点可见性的函数 |
| `onchange` | `OnNodeSelectionChange` | `null` | 节点选择更改时的回调函数 |
| `onfocus` | `((event: FocusEvent) => void) \| null` | `null` | 树获得焦点时的回调函数 |
| `onblur` | `((event: FocusEvent) => void) \| null` | `null` | 树失去焦点时的回调函数 |
| `onContextMenu` | `OnContextMenu` | `null` | 右键上下文菜单处理程序 |
| `checkIsDirectory` | `CheckIsDirectory<any>` | 必需 | 判断节点是否为目录的函数 |
| `version` | `number \| undefined` | `undefined` | 用于触发响应式更新的版本号（从 TreeNodes 对象传入） |
| `class` | `string` | `""` | CSS 类名 |

## 类型定义

### TreeNode<T>
```typescript
class TreeNode<T> implements ITreeNode<T> {
  readonly item: T;                           // 节点的数据对象
  get children(): Array<TreeNode<T>> | null;  // 子节点（浅拷贝）
  get level(): number;                        // 节点层级（根节点为 0）
  expand: boolean;                            // 是否展开（显示子节点）
  loading: boolean;                           // 是否正在加载（用于懒加载）
  readonly parent: TreeNode<T> | null;        // 父节点引用

  // 方法
  append(childItem: T | Array<T>): ITreeNode<T> | Array<ITreeNode<T>>;  // 添加子节点
  detach(): void;                                                              // 从父节点中分离
  moveTo(newParent: TreeNode<T>): TreeNode<T>;                                 // 移动到其他父节点
  replace(newItem: T): void;                                                   // 替换节点数据
  removeChildren(): void;                                                      // 删除所有子节点
}
```

### ITreeNode<T>
```typescript
interface ITreeNode<T> {
  expand: boolean;                              // 是否展开
  loading: boolean;                             // 是否正在加载
  get level(): number;                          // 节点层级（根节点为 0）
  get children(): Array<ITreeNode<T>> | null;   // 子节点（只读）
  get item(): T;                                // 节点数据

  // 方法
  append(childItem: T | Array<T>): ITreeNode<T> | Array<ITreeNode<T>>;
  detach(): void;
  moveTo(newParent: ITreeNode<T>): ITreeNode<T>;
  replace(newItem: T): void;
  removeChildren(): void;
}
```

### TreeLazyLoader
```typescript
interface TreeLazyLoader {
  isBranch: (node: ITreeNode<any>) => boolean;              // 判断节点是否可为分支
  load: (node: ITreeNode<any>) => Promise<Array<any>>;      // 加载子节点数据
}
```

### LazyLoader (已弃用)
```typescript
/**
 * @deprecated 使用 TreeLazyLoader 代替。LazyLoader 已弃用，将在未来版本中移除。
 */
type LazyLoader = TreeLazyLoader;
```

### OnNodeSelectionChange
```typescript
type OnNodeSelectionChange = (node: TreeNode<any>) => Promise<boolean>;
```

### NodeVisibleFun
```typescript
type NodeVisibleFun = (node: TreeNode<any>) => boolean;
```

## 示例

### 文件系统树

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let activeNode = null;
  
  const fileSystemData = [
    { id: 1, name: "📁 文档", type: "folder", parent: null },
    { id: 2, name: "📁 图片", type: "folder", parent: null },
    { id: 3, name: "📁 工作", type: "folder", parent: 1 },
    { id: 4, name: "📁 个人", type: "folder", parent: 1 },
    { id: 5, name: "📄 简历.pdf", type: "file", parent: 3 },
    { id: 6, name: "📄 求职信.doc", type: "file", parent: 3 },
    { id: 7, name: "🖼️ 度假.jpg", type: "file", parent: 2 },
    { id: 8, name: "🖼️ 家庭.png", type: "file", parent: 2 }
  ];
  
  const fileTree = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => item.parent === null,
    checkIsDirectory: (node) => node.item.type === 'folder',
    expendDepth: 1 // 默认展开第一层
  });
  
  fileTree.setData(fileSystemData);
  
  function handleFileSelection(node) {
    console.log('选中文件：', node.item.name);
    return true;
  }
  
  function isDirectory(node) {
    return node.item.type === 'folder';
  }
</script>

<div class="file-explorer">
  <h3>文件浏览器</h3>
  <TreeView 
    nodes={fileTree.nodes}
    textField="name"
    bind:activeNode
    onchange={handleFileSelection}
    checkIsDirectory={isDirectory}
  />
</div>

<style>
  .file-explorer {
    width: 300px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
  
  h3 {
    margin: 0 0 12px 0;
    font-size: 16px;
  }
</style>
```

### 组织结构图

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let selectedEmployee = null;
  
  const orgData = [
    { id: 1, name: "首席执行官 - 张伟", position: "首席执行官", manager: null },
    { id: 2, name: "技术总监 - 李娜", position: "技术总监", manager: 1 },
    { id: 3, name: "财务总监 - 王强", position: "财务总监", manager: 1 },
    { id: 4, name: "开发经理 - 刘芳", position: "开发经理", manager: 2 },
    { id: 5, name: "质量经理 - 陈明", position: "质量经理", manager: 2 },
    { id: 6, name: "开发者 - 赵丽", position: "高级开发者", manager: 4 },
    { id: 7, name: "开发者 - 孙洋", position: "初级开发者", manager: 4 },
    { id: 8, name: "质量工程师 - 周洁", position: "质量工程师", manager: 5 }
  ];
  
  const orgChart = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'manager',
    checkIsRoot: (item) => item.manager === null,
    checkIsDirectory: (node) => node.children && node.children.length > 0,
    expendDepth: 2
  });
  
  orgChart.setData(orgData);
  
  function handleEmployeeSelection(node) {
    selectedEmployee = node.item;
    return true;
  }
  
  function isManager(node) {
    return node.children && node.children.length > 0;
  }
</script>

<div class="org-chart">
  <h3>组织结构图</h3>
  <TreeView 
    nodes={orgChart.nodes}
    textField="name"
    onchange={handleEmployeeSelection}
    checkIsDirectory={isManager}
  />
  
  {#if selectedEmployee}
    <div class="employee-details">
      <h4>员工详情</h4>
      <p><strong>姓名：</strong> {selectedEmployee.name}</p>
      <p><strong>职位：</strong> {selectedEmployee.position}</p>
    </div>
  {/if}
</div>

<style>
  .org-chart {
    width: 400px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
  
  .employee-details {
    margin-top: 16px;
    padding: 12px;
    background: #f8f9fa;
    border-radius: 4px;
  }
  
  .employee-details h4 {
    margin: 0 0 8px 0;
  }
  
  .employee-details p {
    margin: 4px 0;
  }
</style>
```

### 延迟加载

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let activeNode = null;
  
  // 初始根节点
  const initialData = [
    { id: 1, name: "文件夹 A", hasChildren: true },
    { id: 2, name: "文件夹 B", hasChildren: true },
    { id: 3, name: "文件 1.txt", hasChildren: false }
  ];
  
  const treeNodes = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => !item.parent,
    checkIsDirectory: (node) => node.item.hasChildren
  });
  
  treeNodes.setData(initialData);
  
  // 延迟加载配置
  const lazyLoader = {
    isBranch: (node) => node.item.hasChildren,
    
    load: async (node) => {
      // 模拟 API 调用
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      // 生成子节点
      const children = [];
      for (let i = 1; i <= 3; i++) {
        children.push({
          id: `${node.item.id}-${i}`,
          name: `${node.item.name} - 子节点 ${i}`,
          parent: node.item.id,
          hasChildren: Math.random() > 0.7 // 30% 概率有子节点
        });
      }
      
      return children.map(child => ({ item: child, children: null }));
    }
  };
  
  function handleSelection(node) {
    console.log('选中：', node.item.name);
    return true;
  }
  
  function isDirectory(node) {
    return node.item.hasChildren;
  }
</script>

<div class="lazy-tree">
  <h3>延迟加载树</h3>
  <TreeView 
    nodes={treeNodes.nodes}
    textField="name"
    bind:activeNode
    {lazyLoader}
    onchange={handleSelection}
    checkIsDirectory={isDirectory}
  />
</div>

<style>
  .lazy-tree {
    width: 300px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
</style>
```

### 带上下文菜单

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  import ContextMenu from "@ticatec/uniface-element/ContextMenu";
  
  let activeNode = null;
  let contextMenu;
  
  const menuData = [
    { id: 1, name: "根文件夹", type: "folder", parent: null },
    { id: 2, name: "子文件夹", type: "folder", parent: 1 },
    { id: 3, name: "文档.txt", type: "file", parent: 2 }
  ];
  
  const menuTree = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => item.parent === null,
    checkIsDirectory: (node) => node.item.type === 'folder'
  });
  
  menuTree.setData(menuData);
  
  function handleContextMenu(event, node) {
    const isFolder = node.item.type === 'folder';
    
    const menuItems = [
      {
        text: "打开",
        action: () => console.log('打开：', node.item.name)
      },
      {
        text: "重命名",
        action: () => console.log('重命名：', node.item.name)
      },
      ...(isFolder ? [{
        text: "添加文件",
        action: () => console.log('添加文件到：', node.item.name)
      }] : []),
      {
        text: "删除",
        action: () => console.log('删除：', node.item.name)
      }
    ];
    
    contextMenu.show(event, menuItems);
  }
  
  function isDirectory(node) {
    return node.item.type === 'folder';
  }
</script>

<div class="context-tree">
  <h3>右键显示上下文菜单</h3>
  <TreeView 
    nodes={menuTree.nodes}
    textField="name"
    bind:activeNode
    onContextMenu={handleContextMenu}
    checkIsDirectory={isDirectory}
  />
</div>

<ContextMenu bind:this={contextMenu} />

<style>
  .context-tree {
    width: 300px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
</style>
```

### 带自定义可见性过滤

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let activeNode = null;
  let showHidden = false;
  
  const fileData = [
    { id: 1, name: "public", hidden: false, parent: null },
    { id: 2, name: ".env", hidden: true, parent: null },
    { id: 3, name: "index.js", hidden: false, parent: 1 },
    { id: 4, name: ".gitignore", hidden: true, parent: 1 },
    { id: 5, name: "config.json", hidden: false, parent: 1 }
  ];
  
  const fileTree = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => item.parent === null,
    checkIsDirectory: (node) => node.children && node.children.length > 0
  });
  
  fileTree.setData(fileData);
  
  function isVisible(node) {
    return showHidden || !node.item.hidden;
  }
  
  function isDirectory(node) {
    return node.children && node.children.length > 0;
  }
</script>

<div class="filtered-tree">
  <div class="controls">
    <label>
      <input type="checkbox" bind:checked={showHidden} />
      显示隐藏文件
    </label>
  </div>
  
  <TreeView 
    nodes={fileTree.nodes}
    textField="name"
    bind:activeNode
    {isVisible}
    checkIsDirectory={isDirectory}
  />
</div>

<style>
  .filtered-tree {
    width: 300px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
  
  .controls {
    margin-bottom: 12px;
  }
  
  label {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 14px;
  }
</style>
```

### 带事件处理程序

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let activeNode = null;
  let isFocused = false;
  let selectionLog = [];
  
  const eventData = [
    { id: 1, name: "节点 1", parent: null },
    { id: 2, name: "节点 2", parent: null },
    { id: 3, name: "子节点 1", parent: 1 },
    { id: 4, name: "子节点 2", parent: 1 }
  ];
  
  const eventTree = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => item.parent === null,
    checkIsDirectory: (node) => node.children && node.children.length > 0
  });
  
  eventTree.setData(eventData);
  
  function handleSelectionChange(node) {
    selectionLog = [...selectionLog, `选中：${node.item.name} 在 ${new Date().toLocaleTimeString()}`];
    return true;
  }
  
  function handleFocus(event) {
    console.log('树已聚焦');
    isFocused = true;
  }
  
  function handleBlur(event) {
    console.log('树已失焦');
    isFocused = false;
  }
  
  function isDirectory(node) {
    return node.children && node.children.length > 0;
  }
</script>

<div class="event-tree">
  <h3>事件处理演示</h3>
  <p>树当前 {isFocused ? '已聚焦' : '未聚焦'}</p>
  
  <TreeView 
    nodes={eventTree.nodes}
    textField="name"
    bind:activeNode
    onchange={handleSelectionChange}
    onfocus={handleFocus}
    onblur={handleBlur}
    checkIsDirectory={isDirectory}
  />
  
  <div class="log">
    <h4>选择日志：</h4>
    {#each selectionLog.slice(-5) as entry}
      <p>{entry}</p>
    {/each}
  </div>
</div>

<style>
  .event-tree {
    width: 400px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
  
  .log {
    margin-top: 16px;
    padding: 12px;
    background: #f8f9fa;
    border-radius: 4px;
    max-height: 150px;
    overflow-y: auto;
  }
  
  .log h4 {
    margin: 0 0 8px 0;
    font-size: 14px;
  }
  
  .log p {
    margin: 2px 0;
    font-size: 12px;
    font-family: monospace;
  }
</style>
```

### 地理层级

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";
  
  let activeLocation = null;
  
  const locationData = [
    { code: "CN", name: "中国", type: "country", parent: null },
    { code: "US", name: "美国", type: "country", parent: null },
    { code: "CN-BJ", name: "北京", type: "province", parent: "CN" },
    { code: "CN-SH", name: "上海", type: "province", parent: "CN" },
    { code: "US-CA", name: "加利福尼亚", type: "state", parent: "US" },
    { code: "US-NY", name: "纽约", type: "state", parent: "US" },
    { code: "CN-BJ-HD", name: "海淀区", type: "district", parent: "CN-BJ" },
    { code: "CN-BJ-CY", name: "朝阳区", type: "district", parent: "CN-BJ" },
    { code: "US-CA-SF", name: "旧金山", type: "city", parent: "US-CA" },
    { code: "US-CA-LA", name: "洛杉矶", type: "city", parent: "US-CA" }
  ];
  
  const locationTree = new TreeNodes({
    keyField: 'code',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => item.parent === null,
    checkIsDirectory: (node) => {
      const hasChildren = locationData.some(loc => loc.parent === node.item.code);
      return hasChildren;
    },
    expendDepth: 1
  });
  
  locationTree.setData(locationData);
  
  function handleLocationSelection(node) {
    activeLocation = node.item;
    return true;
  }
  
  function isDirectory(node) {
    return locationData.some(loc => loc.parent === node.item.code);
  }
  
  function getLocationIcon(type) {
    switch (type) {
      case 'country': return '🌍';
      case 'province':
      case 'state': return '🏛️';
      case 'district':
      case 'city': return '🏙️';
      default: return '📍';
    }
  }
  
  function displayLocationName(item) {
    return `${getLocationIcon(item.type)} ${item.name}`;
  }
</script>

<div class="location-tree">
  <h3>地理层级</h3>
  <TreeView 
    nodes={locationTree.nodes}
    textField={displayLocationName}
    onchange={handleLocationSelection}
    checkIsDirectory={isDirectory}
  />
  
  {#if activeLocation}
    <div class="location-details">
      <h4>选中的位置</h4>
      <p><strong>名称：</strong> {activeLocation.name}</p>
      <p><strong>代码：</strong> {activeLocation.code}</p>
      <p><strong>类型：</strong> {activeLocation.type}</p>
    </div>
  {/if}
</div>

<style>
  .location-tree {
    width: 350px;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
  
  .location-details {
    margin-top: 16px;
    padding: 12px;
    background: #f8f9fa;
    border-radius: 4px;
  }
  
  .location-details h4 {
    margin: 0 0 8px 0;
  }
  
  .location-details p {
    margin: 4px 0;
  }
</style>
```

## 功能

- **层级显示**：清晰的树结构，支持可展开/折叠的节点
- **延迟加载**：按需加载子节点，适合大数据集
- **上下文菜单**：支持右键自定义操作
- **键盘导航**：完整的键盘可访问性支持
- **自定义可见性**：根据自定义标准过滤节点
- **选择管理**：跟踪和控制节点选择
- **事件处理**：全面的交互事件系统
- **灵活数据**：支持任何数据结构，配置灵活映射

## 节点管理

树视图组件与 `TreeNodes` 工具类配合使用，管理层级数据：

```typescript
const treeNodes = new TreeNodes({
  keyField: 'id',           // 唯一标识字段
  textField: 'name',        // 显示文本字段
  parentKeyField: 'parent', // 父节点引用字段
  checkIsRoot: (item) => item.parent === null,
  checkIsDirectory: (node) => node.children != null,
  expendDepth: 1           // 初始展开深度
});

treeNodes.setData(flatData); // 将扁平数据转换为树结构

// 动态添加节点
treeNodes.append(newItem);

// 移除节点
treeNodes.removeItem(oldItem);

// 移动节点
treeNodes.moveTo(itemToMove, newParentId);
```

## 动态更新数据

TreeView 的每个节点都提供了便捷的方法来操作树结构：

### 节点方法

所有从 TreeNodes 获取的节点都包含以下方法：

- `node.append(childItem)` - 添加子节点到当前节点
- `node.remove()` - 删除当前节点（从父节点中移除）
- `node.replace(newItem)` - 替换当前节点的数据
- `node.moveTo(newParentId)` - 将当前节点移动到另一个父节点下

### 使用示例

```svelte
<script>
  import TreeView from "@ticatec/uniface-element/TreeView";
  import TreeNodes from "@ticatec/uniface-element/TreeNodes";

  let activeNode = null;
  let zones;

  const treeNodes = new TreeNodes({
    keyField: 'id',
    textField: 'name',
    parentKeyField: 'parent',
    checkIsRoot: (item) => item.parent === null,
    checkIsDirectory: (node) => node.children != null
  });

  treeNodes.setData(initialData);
  zones = treeNodes;

  // 添加子节点到选中的节点
  function addNode() {
    if (!activeNode) return;

    const newItem = {
      id: Date.now(),
      name: "新节点",
      parent: activeNode.item.id
    };

    // 方式1：直接在节点上调用 append 方法
    activeNode.append(newItem);

    // 方式2：使用 TreeNodes 的 append 方法（会自动找到父节点）
    // treeNodes.append(newItem);

    // version 会自动更新，TreeView 会重新渲染
  }

  // 删除选中的节点
  function deleteNode() {
    if (activeNode) {
      // 直接在节点上调用 remove 方法
      activeNode.remove();
      activeNode = null;
    }
  }

  // 替换节点的数据
  function renameNode() {
    if (activeNode) {
      const newItem = {
        ...activeNode.item,
        name: "重命名后的节点"
      };
      activeNode.replace(newItem);
    }
  }

  // 移动节点到另一个父节点
  function moveNode() {
    if (activeNode) {
      const newParentId = prompt("输入新的父节点 ID:");
      if (newParentId) {
        activeNode.moveTo(newParentId);
      }
    }
  }
</script>

<div>
  <button on:click={addNode} disabled={!activeNode}>添加子节点</button>
  <button on:click={deleteNode} disabled={!activeNode}>删除节点</button>
  <button on:click={renameNode} disabled={!activeNode}>重命名</button>
  <button on:click={moveNode} disabled={!activeNode}>移动节点</button>

  <TreeView
    nodes={treeNodes.nodes}
    version={treeNodes.version}
    textField="name"
    bind:activeNode
    checkIsDirectory={node => node.children != null}
  />
</div>
```

### TreeNodes 类方法

如果你只有一个数据项，可以使用 `TreeNodes` 类的方法：

```typescript
// 添加新节点（会自动根据 parentKeyField 找到父节点）
treeNodes.append(newItem);

// 注意：删除、替换、移动操作建议直接在节点上调用方法
// 因为这些操作通常针对特定的节点实例
```

### Lazy 节点注意事项

对于使用 lazyLoader 的节点（尚未展开加载的节点），直接添加子节点会给出警告：

```javascript
// 如果父节点是 lazy 节点且尚未加载
if (parentNode.children == null) {
  console.warn('Cannot add child: parent is a lazy-loaded node');
  // 节点会被添加到 nodeMap，但不会添加到 parent.children
  // 这样不会干扰后续的 lazyLoader 行为
}
```

建议在使用前先展开节点，触发 lazyLoader 加载子节点。

**重要提示**：必须将 `treeNodes.version` 传递给 `TreeView` 的 `version` 属性，以确保在数据变化时组件能够正确重新渲染。

## 可访问性

- 支持使用箭头键和回车键进行键盘导航
- 提供适当的 ARIA 属性，兼容屏幕阅读器
- 焦点管理和视觉指示器
- 语义化 HTML 结构
- 支持 Tab 导航

## 最佳实践

1. **数据结构**：在数据中保持一致的字段名称
2. **性能**：为大数据集实现延迟加载
3. **可见性**：使用 `isVisible` 属性高效过滤节点
4. **选择**：在选择更改处理程序中始终返回布尔值
5. **上下文菜单**：提供上下文相关的菜单项
6. **可访问性**：确保适当的键盘导航支持

## 浏览器支持

- 支持完整事件处理的现代浏览器
- 兼容 Svelte 5+
- 支持键盘导航
- 触控友好界面
- 完整的 TypeScript 支持

## 相关组件

- `TreeNodes` - 树数据结构工具
- `ContextMenu` - 右键上下文菜单组件
- `LazyLoader` - 异步数据加载接口