Files

279 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TreeSelect 树选择
## 概述
树选择(TreeSelect)是一种将树形数据与下拉选择结合的复合控件,用于从层级结构的数据中选择一个或多个值。它本质上是将 `TreeView` 嵌入到下拉弹窗中,让用户在不占用大量页面空间的前提下,浏览和选择树形数据。
TreeSelect 常见于:组织架构选择、分类目录选择、地区层级选择、权限树选择等场景——凡是数据具有父子嵌套关系、且需要通过下拉方式呈现的,都适合使用 TreeSelect。
AtomUI 的 `TreeSelect` 控件对齐了 [Ant Design 5.0 TreeSelect](https://ant.design/components/tree-select-cn) 的设计规范,在 .NET / Avalonia 平台上提供与 Ant Design React 版一致的视觉体验和交互行为。
---
## 设计原理
### Ant Design 的 TreeSelect 设计哲学
Ant Design 对 TreeSelect 的定位是:**「类似 Select 的选择控件,可选择的数据结构是一个树形结构时,可以使用 TreeSelect」**。与普通的 `Select` 相比,TreeSelect 的核心差异在于:
- **数据源是树形的**:每个选项可以有子选项,形成多层嵌套。
- **支持展开/折叠**:用户可以通过展开/折叠来逐层浏览。
- **支持勾选模式**:除了单选/多选之外,还支持 Checkbox 勾选方式,并提供 `ShowCheckedStrategy` 控制显示策略。
**三种选择模式**:
| 模式 | 设计意图 | 典型用途 |
|---|---|---|
| 🔘 **单选** | 默认模式,点击节点即选中,弹窗自动关闭 | 选择一个分类、一个部门 |
| ✅ **多选(Multiple)** | 允许选择多个节点,选中项以 Tag 形式展示 | 选择多个标签、多个地区 |
| ☑️ **勾选(Checkable)** | 使用 Checkbox 进行勾选,支持父子联动 | 权限树、批量分类选择 |
**勾选策略(ShowCheckedStrategy)**:
| 策略 | 行为 |
|---|---|
| `All` | 展示所有勾选的节点(默认) |
| `ShowParent` | 当所有子节点都勾选时,仅展示父节点 |
| `ShowChild` | 仅展示叶子节点 |
### AbstractSelect 基类能力
AtomUI 的 `TreeSelect` 继承自 `AbstractSelect`,这是一个桌面端选择类控件的公共基类(定义在 `AtomUI.Desktop.Controls` 中),它提供了所有选择类控件共享的基础设施。
**AbstractSelect 的核心职责:**
`AbstractSelect` 继承自 Avalonia 的 `TemplatedControl`,并实现了 7 个共享接口。其核心行为是:**管理下拉弹窗的打开/关闭、提供统一的输入框装饰(AddOnDecoratedBox)、处理筛选/清除/加载/状态反馈等通用逻辑**。继承链为:
```
Control → TemplatedControl → AbstractSelect
```
**AbstractSelect 提供的基础能力:**
| 能力 | 说明 |
|---|---|
| 下拉弹窗管理 | `IsDropDownOpen` 控制弹窗开关,自动处理 `Opening` / `Opened` / `Closing` / `Closed` 生命周期 |
| 占位文本 | `PlaceholderText` + `PlaceholderForeground` 提供空状态提示 |
| 筛选支持 | `IsFilterEnabled` + `FilterValue` 提供搜索筛选基础 |
| 清除按钮 | `IsAllowClear` 支持一键清除选中项 |
| AddOn 装饰 | `LeftAddOn` / `RightAddOn` / `ContentLeftAddOn` / `ContentRightAddOn` 提供前后缀附加内容 |
| 弹窗位置 | `Placement` 控制弹窗相对于控件的弹出方向 |
| 样式变体 | `StyleVariant`(Outline / Filled / Borderless / Underlined) |
| 状态反馈 | `Status`(Default / Error / Warning)配合表单验证 |
| 尺寸系统 | `SizeType`(Small / Middle / Large)统一尺寸 |
| 多选限制 | `MaxCount` 限制最大可选数量,`MaxTagCount` 限制 Tag 最大显示数量 |
| 空状态指示 | `EmptyIndicator` 在无数据时展示空提示 |
| 加载状态 | `IsLoading` 显示旋转加载图标 |
| 动画控制 | `IsMotionEnabled` 控制弹窗展开/收起动画 |
| 紧凑空间 | `ICompactSpaceAware` 支持在 `Space.Compact` 中使用 |
| 表单集成 | `IFormItemAware` 参与 Form 验证流程 |
### AtomUI TreeSelect 的扩展设计
TreeSelect 在 AbstractSelect 的基础上,增加了树形数据特有的能力:
| 扩展能力 | 实现方式 | 设计动机 |
|---|---|---|
| **树形数据源** | `ItemsSource`(`IEnumerable<ITreeItemNode>`)+ `Items` 集合 | 支持树形数据绑定和直接操作 |
| **自定义模板** | `ItemTemplate`(`IDataTemplate`) | 自定义节点渲染方式 |
| **单选/多选/勾选** | `IsMultiple` + `IsTreeCheckable` 属性 | 三种选择模式灵活切换 |
| **勾选策略** | `ShowCheckedStrategy` 枚举 | 控制勾选模式下 Tag 的显示策略 |
| **搜索筛选** | `Filter`(`ITreeItemFilter`)+ `FilterHighlightStrategy` | 支持关键字搜索并高亮匹配项 |
| **异步加载** | `DataLoader`(`ITreeItemNodeLoader`) | 支持树节点按需异步加载 |
| **树线/图标** | `IsShowTreeLine` / `IsShowIcon` / `IsShowLeafIcon` / `IsShowLine` | 控制树形结构的视觉展示 |
| **展开控制** | `IsDefaultExpandAll` / `TreeDefaultExpandedPaths` | 控制初始展开状态 |
| **严格勾选** | `IsTreeCheckStrictly` | 父子节点勾选互不关联 |
| **选中项** | `SelectedItem`(单选)/ `SelectedItems`(多选)| 双向绑定选中值 |
| **弹窗匹配宽度** | `IsPopupMatchSelectWidth`(继承自 AbstractSelect) | 控制弹窗是否匹配控件宽度 |
| **最大选择数量** | `MaxCount`(继承自 AbstractSelect) | 达到上限后禁止继续选择 |
---
## 功能详解
### 选择模式
TreeSelect 根据 `IsMultiple` 和 `IsTreeCheckable` 的组合,提供三种选择模式:
| `IsMultiple` | `IsTreeCheckable` | 效果 |
|---|---|---|
| `false` | `false` | **单选模式**:点击节点即选中,下拉自动关闭 |
| `true` | `false` | **多选模式**:可选择多个节点,选中项以 Tag 展示 |
| `true`* | `true` | **勾选模式**:显示 Checkbox,支持父子联动勾选 |
> *注:设置 `IsTreeCheckable = true` 会自动将 `IsMultiple` 设为 `true`。
### 勾选策略(ShowCheckedStrategy)
在勾选模式下,`ShowCheckedStrategy` 控制选中项在选择框中的展示方式:
| 策略值 | 行为 | 典型效果 |
|---|---|---|
| `All` | 展示所有被勾选的节点 | 勾选了父节点和所有子节点,则全部显示为 Tag |
| `ShowParent` | 当一个父节点的所有子节点都被勾选时,仅显示父节点 | 简化展示,减少 Tag 数量 |
| `ShowChild` | 仅展示被勾选的叶子节点 | 只关注最终数据,忽略中间层级 |
### 搜索筛选
当 `IsFilterEnabled = true` 时,TreeSelect 在输入框中提供搜索功能:
- 单选模式:输入框变为搜索框,打开弹窗后可直接输入关键字。
- 多选模式:在 Tag 区域末尾提供内嵌搜索输入框。
- 筛选逻辑由 `Filter`(`ITreeItemFilter` 接口)控制,默认使用 `DefaultTreeItemFilter`。
- `FilterHighlightStrategy` 控制匹配高亮方式(高亮整行、加粗匹配文字、展开匹配路径、隐藏不匹配项等)。
- `FilterHighlightForeground` 控制高亮颜色,默认使用 `ColorPrimary`。
### 异步加载
通过 `DataLoader`(`ITreeItemNodeLoader`)属性,TreeSelect 支持树节点按需异步加载:
- 展开一个非叶子节点时,自动调用 `DataLoader` 加载其子节点。
- 加载过程中节点显示旋转加载图标。
- 适用于数据量大、不适合一次性加载全部树形数据的场景。
### 最大选择数量(MaxCount)
在多选/勾选模式下,可通过 `MaxCount` 限制最大可选数量:
- 当选择数量达到 `MaxCount` 时,未选中的节点变为禁用状态。
- 配合 `IsShowMaxCountIndicator` 可在右侧显示已选/最大数量指示器。
### 树线与图标
| 属性 | 效果 |
|---|---|
| `IsShowTreeLine` | 在节点之间显示连接线 |
| `IsShowLine` | 控制 TreeView 中的连接线显示 |
| `IsShowIcon` | 显示节点图标 |
| `IsShowLeafIcon` | 显示叶子节点图标 |
| `IsSwitcherRotation` | 展开/折叠按钮使用旋转动画(而非切换图标) |
---
## 与 Ant Design 规范的对齐程度
| 特性 | Ant Design (React) | AtomUI (Avalonia) | 对齐情况 |
|---|---|---|---|
| 树形数据源 `treeData` | ✅ 数组对象 | ✅ `ItemsSource`(`IEnumerable<ITreeItemNode>`) | ✅ 完全对齐 |
| 单选 | ✅ 默认模式 | ✅ 默认模式 | ✅ 完全对齐 |
| 多选 `multiple` | ✅ 布尔属性 | ✅ `IsMultiple` 属性 | ✅ 完全对齐 |
| 勾选 `treeCheckable` | ✅ 布尔属性 | ✅ `IsTreeCheckable` 属性 | ✅ 完全对齐 |
| 勾选策略 `showCheckedStrategy` | ✅ `SHOW_ALL / SHOW_PARENT / SHOW_CHILD` | ✅ `TreeSelectCheckedStrategy` 枚举 | ✅ 完全对齐 |
| 搜索筛选 `showSearch` / `filterTreeNode` | ✅ 属性 + 回调 | ✅ `IsFilterEnabled` + `Filter` 接口 | ✅ 完全对齐 |
| 异步加载 `loadData` | ✅ 回调函数 | ✅ `DataLoader`(`ITreeItemNodeLoader`) | ✅ 完全对齐 |
| 默认展开所有 `treeDefaultExpandAll` | ✅ 布尔属性 | ✅ `IsDefaultExpandAll` | ✅ 完全对齐 |
| 默认展开指定节点 `treeDefaultExpandedKeys` | ✅ 数组 | ✅ `TreeDefaultExpandedPaths` | ✅ 完全对齐 |
| 显示树线 `treeLine` | ✅ 布尔/对象 | ✅ `IsShowTreeLine` / `IsShowLine` | ✅ 完全对齐 |
| 显示图标 `treeIcon` | ✅ 布尔属性 | ✅ `IsShowIcon` | ✅ 完全对齐 |
| 严格勾选 `treeCheckStrictly` | ✅ 布尔属性 | ✅ `IsTreeCheckStrictly` | ✅ 完全对齐 |
| 清除按钮 `allowClear` | ✅ 布尔属性 | ✅ `IsAllowClear` | ✅ 完全对齐 |
| 占位文本 `placeholder` | ✅ 字符串 | ✅ `PlaceholderText` | ✅ 完全对齐 |
| 弹窗匹配宽度 `popupMatchSelectWidth` | ✅ 布尔/数值 | ✅ `IsPopupMatchSelectWidth` | ✅ 完全对齐 |
| 弹窗位置 `placement` | ✅ 字符串 | ✅ `Placement` 枚举 | ✅ 完全对齐 |
| 最大选择数 `maxCount` | ✅ 数值 | ✅ `MaxCount` | ✅ 完全对齐 |
| 样式变体 `variant` | ✅ `outlined / filled / borderless` | ✅ `StyleVariant`(Outline / Filled / Borderless / Underlined) | ✅ 超越对齐(额外支持 Underlined) |
| 状态 `status` | ✅ `error / warning` | ✅ `Status` 枚举 | ✅ 完全对齐 |
| 尺寸 `size` | ✅ `large / middle / small` | ✅ `SizeType` 属性 | ✅ 完全对齐 |
| 前后缀 | ✅ `suffixIcon` | ✅ `LeftAddOn` / `RightAddOn` / `ContentLeftAddOn` / `SuffixIcon` | ✅ 超越对齐 |
| 虚拟滚动 `virtual` | ✅ 支持 | ⚠️ 暂未支持 | ⚠️ 待支持 |
| 下拉菜单自定义渲染 `dropdownRender` | ✅ 回调函数 | ❌ 暂不支持 | ⚠️ 待支持 |
| `treeTitleRender` | ✅ 回调函数 | ✅ `ItemTemplate` | ✅ 完全对齐(方式不同) |
---
## 继承关系
```
Avalonia.Controls.Primitives.TemplatedControl
└── AtomUI.Desktop.Controls.AbstractSelect
├── implements IMotionAwareControl
├── implements ISizeTypeAware
├── implements ICompactSpaceAware
├── implements IInputControlStatusAware
├── implements IInputControlStyleVariantAware
├── implements IFormItemAware
└── implements IFormItemFeedbackAware
└── AtomUI.Desktop.Controls.TreeSelect
```
> **注意**:`AbstractSelect` 和 `TreeSelect` 都位于 `AtomUI.Desktop.Controls`。当前 TreeSelect 没有跨平台的抽象基类(即没有 `AtomUI.Controls.AbstractTreeSelect`),这意味着它目前是纯桌面端控件。未来如需支持移动端,可能需要提取共享基类。
**各层级职责划分:**
| 层级 | 提供的能力 |
|---|---|
| `TemplatedControl` | Avalonia 模板化控件基础设施 |
| `AbstractSelect` | 下拉弹窗管理、AddOn 装饰框、筛选/清除/加载、样式变体、状态反馈、尺寸系统、紧凑空间适配、表单集成 |
| `TreeSelect` | 树形数据源、单选/多选/勾选模式、勾选策略、树线/图标控制、异步加载、搜索筛选(树专用)、选中项管理 |
**实现的共享接口(通过 AbstractSelect 继承):**
| 接口 | 定义位置 | 作用 |
|---|---|---|
| `IMotionAwareControl` | `AtomUI.Controls.Shared` | 支持动画开关控制 |
| `ISizeTypeAware` | `AtomUI.Controls.Shared` | 支持 `SizeType`(Small / Middle / Large)三种尺寸 |
| `ICompactSpaceAware` | `AtomUI.Controls.Shared` | 在 `Space.Compact` 中使用时自动调整圆角和边框 |
| `IInputControlStatusAware` | `AtomUI.Controls.Shared` | 支持 `Status`(Default / Error / Warning)状态切换 |
| `IInputControlStyleVariantAware` | `AtomUI.Controls.Shared` | 支持 `StyleVariant`(Outline / Filled / Borderless / Underlined) |
| `IFormItemAware` | `AtomUI.Controls.Shared` | 可作为 `FormItem` 的子控件参与表单验证 |
| `IFormItemFeedbackAware` | `AtomUI.Controls.Shared` | 支持表单验证反馈控件展示 |
---
## 源码位置
| 层级 | 文件路径 | 说明 |
|---|---|---|
| 基类 | `src/AtomUI.Desktop.Controls/Select/AbstractSelect.cs` | 选择类控件公共基类 |
| 控件类 | `src/AtomUI.Desktop.Controls/TreeSelect/TreeSelect.cs` | TreeSelect 主控件 |
| 辅助控件 | `src/AtomUI.Desktop.Controls/TreeSelect/TreeSelectAddOnDecoratedBox.cs` | 内部装饰框(处理多选内边距) |
| 辅助控件 | `src/AtomUI.Desktop.Controls/TreeSelect/TreeSelectTreeView.cs` | 内部 TreeView(处理 CheckBox 切换和最大选择数) |
| 辅助控件 | `src/AtomUI.Desktop.Controls/TreeSelect/TreeViewSelectTreeViewItem.cs` | 内部 TreeViewItem(传递最大选择数状态) |
| 枚举类型 | `src/AtomUI.Desktop.Controls/TreeSelect/TreeSelectCheckedStrategy.cs` | 勾选策略枚举 |
| Token 定义 | `src/AtomUI.Desktop.Controls/TreeSelect/TreeSelectToken.cs` | 组件级 Design Token |
| 转换器 | `src/AtomUI.Desktop.Controls/TreeSelect/Converters/TreeItemNodeConverter.cs` | ITreeItemNode → Header 转换器 |
| 伪类常量 | `src/AtomUI.Desktop.Controls/Select/SelectPseudoClass.cs` | 共享伪类定义 |
| 主题模板 | `src/AtomUI.Desktop.Controls/TreeSelect/Themes/TreeSelectTheme.axaml` | ControlTheme AXAML |
| 主题模板 | `src/AtomUI.Desktop.Controls/TreeSelect/Themes/TreeSelectAddOnDecoratedBoxTheme.axaml` | 装饰框 ControlTheme |
| 主题模板 | `src/AtomUI.Desktop.Controls/TreeSelect/Themes/TreeSelectTreeViewItemTheme.axaml` | TreeViewItem ControlTheme |
| 主题注册 | `src/AtomUI.Desktop.Controls/TreeSelect/Themes/TreeSelectThemes.axaml` | ResourceDictionary 聚合 |
| 模板常量 | `src/AtomUI.Desktop.Controls/TreeSelect/Themes/TreeSelectThemeConstants.cs` | 模板部件名称常量 |
| Gallery 示例 | `controlgallery/AtomUIGallery/ShowCases/Views/DataEntry/TreeSelectShowCase.axaml` | 使用范例 |
| Gallery 代码 | `controlgallery/AtomUIGallery/ShowCases/Views/DataEntry/TreeSelectShowCase.axaml.cs` | 示例 code-behind |
---
## 模板结构
TreeSelect 的 ControlTemplate 由两个核心部分组成:装饰框(选择框外观)和弹窗(下拉树)。
```
Panel
├── TreeSelectAddOnDecoratedBox (PART_AddOnDecoratedBox) ← 选择框外观层
│ ├── ContentRightAddOn ← 右侧附加内容
│ │ ├── SelectMaxCountIndicator ← 最大数量指示器(可选)
│ │ ├── ContentPresenter (ContentRightAddOn) ← 自定义右侧内容(可选)
│ │ └── SelectHandle ← 下拉箭头 / 清除按钮 / 加载图标
│ └── Panel ← 内容区域
│ ├── TextBlock#PlaceholderText ← 占位文本
│ ├── ContentPresenter#SingleSelectResultPresenter ← 单选模式:显示选中项文本
│ ├── SelectFilterTextBox (PART_SingleFilterInput) ← 单选模式:搜索输入框
│ └── SelectTagAwareTextBox#SelectedItemsBox ← 多选模式:Tag 展示 + 搜索输入
└── Popup (PART_Popup) ← 下拉弹窗
└── Border#PopupFrame ← 弹窗容器(背景、圆角、阴影)
└── TreeSelectTreeView (PART_TreeView) ← 内嵌 TreeView
```
**模板设计要点:**
- **单选 vs 多选**:通过 `IsMultiple` 属性切换 `SingleSelectResultPresenter`/`SelectFilterTextBox`(单选)和 `SelectTagAwareTextBox`(多选)的可见性。
- **搜索输入**:单选搜索时,`SelectFilterTextBox` 覆盖在选中文本之上,选中文本降为 Placeholder 色。
- **弹窗 TreeView**:使用自定义的 `TreeSelectTreeView`,继承自标准 `TreeView`,增加了 `IsMaxSelectReached` 状态传递能力。
### 模板部件常量
| 常量名 | 值 | 说明 |
|---|---|---|
| `TreeSelectThemeConstants.PopupPart` | `"PART_Popup"` | 下拉弹窗 |
| `TreeSelectThemeConstants.SingleFilterInputPart` | `"PART_SingleFilterInput"` | 单选搜索输入框 |
| `TreeSelectThemeConstants.TreeViewPart` | `"PART_TreeView"` | 内嵌 TreeView |