Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions specs/033-tree-select-demo-alignment/acceptance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 验收记录

- Flutter 原 PR 基线:`6a69569c104f6576fd6b39253eedbe48faa2b75c`
- 当前 `develop` 基线:`6433854a142a5ca7ee528aba9a1a8e7d72bea3ca`
- 小程序基线:`b60cdc8a1dce1f06dd45cb4e41eefd31c674e514`
- 公开运行页:`https://tdesign.tencent.com/miniprogram/live/m2w/program/miniprogram/#!pages/tree-select/tree-select.html`
- Figma 文件:`TDesign for mobile`,页面 `TreeSelect 树形选择器`。
- Figma Demo 节点:`28591:40742`,尺寸 `378 x 1510`。
- Figma 两列组件变体:`27387:23982`,尺寸 `375 x 336`。
- Figma 一级列变体:`27449:25981`,宽度 `103`。
- API Review:完整路径值、`value + onChanged` 受控模式和数据深度驱动列数已收敛;
不增加公开参数。

## 验证结果

- 最新本地 merge commit:`c9beb6f3`,完成目标分支与 `origin/develop` 的冲突整合;
字体说明同时保留 TreeSelect 与 Stepper,并补齐 TreeSelect 上游来源与子集工具版本。
- 三个场景已改为页面真实使用的独立示例 Widget;生成片段包含 imports、完整 options、
受控 value 与 `onChanged/setState`,不再依赖页面私有成员。
- Flutter 3.32.0:TreeSelect 组件与生成器测试 18 项、Demo 测试 7 项全部通过;
组件包和 example 包 `flutter analyze` 均为 `No issues found`。
- Flutter 3.47.0(latest):TreeSelect 组件与生成器测试 18 项、Demo 测试 7 项全部通过;
组件包和 example 包 `flutter analyze` 均为 `No issues found`。
- 组件生产代码覆盖率:`286/288 = 99.31%`,高于 95% 门槛。
- 路径契约收敛后,Flutter 3.32.0 与 Flutter 3.47.0 的 TreeSelect 组件测试
19 项全部通过;覆盖单选数量、重复路径、同级字符串/null/嵌套值重复,以及
`==` 相等但 `hashCode` 不同的自定义值。
- Flutter 3.32.0 的组件包与 example 包完整 `flutter analyze --fatal-infos`、
Flutter 3.47.0 的 TreeSelect 实现与测试静态分析均为 `No issues found`;
组件生产代码覆盖率为 `311/313 = 99.36%`。
- Flutter 3.32.0 与 Flutter 3.47.0 的 example release Web 构建均通过;
debug 唯一性校验不进入 release 执行路径。
- iPhone 16(iOS 18.2 模拟器)真实点击通过:单选切换分支并选择叶子、多选新增
与取消叶子均正确更新;本轮仅增加契约文档和 debug 校验,不涉及视觉变化,未更新 Golden。
- API 生成配置已登记真实公开类型 `TTreeSelectOption`,生成文档同时包含选项值唯一性、
完整路径、单选数量和重复路径约束。
- 同级选项唯一性改用与路径解析一致的 `==` 线性比较;自定义值在 `==` 相等但
`hashCode` 不同的情况下仍会被 debug 校验拒绝,不依赖业务类型的哈希实现。
- `dart run tool/generate_example_code.dart --check` 通过。
- Linux `amd64` + Flutter 3.32.0 重新生成明暗两张 Demo Golden,并在同一容器
去掉 `--update-goldens` 复跑,2 项全部通过。
- 合并最新 develop 后再次在固定 Linux `amd64` + Flutter 3.32.0 容器中仅比较
明暗 Golden,2 项全部通过,未更新基线。
- 本地 Flutter Web 页面真实点击通过:单选切换到 `广东省 / 云浮市`、多选新增
`广东省 / 深圳市`、三列切换到 `广东省 / 深圳市 / 盐田区`,服务端无异常输出。
- “查看代码”仅在非 Web 端展示;Widget 级真实入口点击已逐个打开三个底部面板,
并核对面板内容与对应生成片段完全一致。
- TreeSelect 专用字体子集已覆盖页面可见文案,人工查看明暗 Golden 无缺字符号;
子集 SHA-256 为
`1cbb5418c4cd91a103deb2a28f68dacb1e895a6efcb8f2f5e8198d67d38ac7ca`。
- 原 PR 在 2026-09-01 的 CI 结果不代表当前合并与修改后的结果;推送后的远端 CI
与 CodeBuddy Review 须以新 head 的实际结果为准。

## 未验证项

- Android/iOS 系统字体逐像素差异不由 Linux Golden 证明。
- 小程序只作为交互流程参考,Flutter 不引入小程序的 `keys` 映射或非受控默认值 API。
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions specs/033-tree-select-demo-alignment/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Plan

1. 固定 Figma 设计节点、小程序版本和 Flutter 实现基线。
2. 合并最新 `develop`,把 TreeSelect 登记迁移到集中测试 manifest。
3. 按设计稿修正 Demo 数据、初始状态、默认列宽和三列溢出。
4. 补齐单选、多选、三列、Theme 插值、查看代码、覆盖率和明暗 Golden 证据。
5. 在 Flutter 3.32.0 与 latest 运行严格 analyze 和非视觉回归,Golden 只在
Flutter 3.32.0 Linux 更新和复验。
29 changes: 29 additions & 0 deletions specs/033-tree-select-demo-alignment/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# TreeSelect 设计与公开 Demo 对齐

## 目标

以 TDesign for mobile 设计稿为视觉和文案权威基线,对齐基础、多选和三列公开示例;
小程序只用于校验逐级切换、单选替换和多选切换流程。

## API Review

- `TTreeSelectOption` 以不可变递归节点表达任意深度,不增加小程序 keys 映射层。
- 同一层级的 `TTreeSelectOption.value` 必须按 `==` 语义保持唯一,debug 模式拒绝歧义配置;
校验不依赖业务值的 `hashCode`,与路径解析和选中比较保持一致。
- `value` 使用根到叶的完整路径列表;单选和多选共享同一稳定值类型,`multiple` 只改变选择数量。
- 单选模式 debug 校验最多一条路径;暂时无法解析到叶子的受控路径不显示选中态,并回退到首个可用分支。
- `value + onChanged` 为严格受控模式,空 callback 表达禁用,不引入 `defaultValue`。
- 三列是数据深度产生的布局结果,不增加列数参数。
- 设计稿的 375 px 容器中,两列宽度为 `103 + 272`,三列宽度为
`103 + 103 + 169`;默认不产生水平滚动。
- 修正默认列宽会改变既有默认布局,属于 breaking change;公开构造参数和值类型不变。

## 行为契约

- Demo 顺序、标题、说明、六行省市数据、初始选中项与设计稿一致。
- 基础与多选初始路径均为 `广东省 / 汕尾市`;多选继续点选其他城市时追加完整路径,再次点选时移除。
- 三列初始路径为 `广东省 / 深圳市 / 南山区`,展示六个区级选项。
- 三个公开实例各自持有受控状态,并作为页面真实渲染与“查看代码”的共同来源。
- “查看代码”片段包含 imports、完整 options、value 与回调,可脱离 Demo 页面独立复用。
- 组件面板在 Demo 容器中无外层左缩进;内部文字保留 16 px 间距。
- 一级和中间分支的活动文字使用主色,叶子文字保持主文字色并使用主色勾选图标。
13 changes: 13 additions & 0 deletions specs/033-tree-select-demo-alignment/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Tasks

- [x] DONE 固定 Figma、小程序与 Flutter 基线
- [x] DONE 合并最新 develop 并迁移集中测试 manifest
- [x] DONE 修正 Demo 数据、初始状态和生产组件默认列宽
- [x] DONE 更新生成片段、功能测试和 Linux 3.32 Golden
- [x] DONE 完成双版本 analyze、组件/Demo 回归与覆盖率
- [x] DONE 创建本地 develop merge commit
- [x] DONE 提交并推送 TreeSelect 对齐改动
- [x] DONE 请求 CNB CodeBuddy 聚焦 Review 并跟进结果
- [x] DONE 合并最新 develop 并解决 TreeSelect/Stepper 字体说明冲突
- [x] DONE 按 Review 修复三个“查看代码”片段的自包含契约与字体溯源
- [x] DONE 复跑双版本测试、严格 analyze、Linux Golden 与浏览器点击验收
30 changes: 30 additions & 0 deletions specs/033-tree-select-demo-alignment/visual-comparison.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# TreeSelect 视觉与交互对齐

## 权威顺序

1. Figma `TDesign for mobile` / `TreeSelect 树形选择器`。
2. Figma Demo 直接使用的 TreeSelect 组件变体和 item 变体。
3. 小程序公开 Demo 只用于验证操作流程。

## 逐项对比

| 契约 | Figma 目标 | Flutter 处理 |
| --- | --- | --- |
| 页面文案 | `TreeSelect 树形选择器`;`用于多层级数据的逐级选择。` | 直接使用设计稿文案 |
| 公开分组 | `01 组件类型`:基础、多选;`02 组件状态`:三列 | 三个 ExampleItem 顺序与标题对齐 |
| 两列数据 | 六个省级行,广东省展示六个市级行 | Demo 使用同一列表和顺序 |
| 初始选中 | 基础和多选均为 `广东省 / 汕尾市` | 页面持有完整路径 |
| 三列数据 | `广东省 / 深圳市 / 南山区`,每列在目标状态可见 6 行 | 递归 options 自然产生第三列 |
| 容器 | 375 px 宽、336 px 高,行高 56 px | 保持 6 行高度 |
| 两列宽度 | `103 + 272` | 默认宽度总和为 375 |
| 三列宽度 | `103 + 103 + 169` | 默认不产生水平滚动 |
| 活动分支 | 一级和中间列文字为主色 | 组件内部统一解析 |
| 选中叶子 | 主文字色 + 24 px 主色勾选 | 保持叶子文字色,使用 `TIcons.check` |
| Demo 外边界 | 组件面板贴齐页面 Demo 容器 | 复用 `ExamplePage.compactDemo` |

## 小程序交互取舍

- 合理复用:点击分支只切换下一列;单选点击叶子替换路径;多选点击叶子切换该完整路径。
- Flutter 调整:使用声明式 `value + onChanged`,不复制小程序的非受控 `defaultValue`、
`keys` 映射和事件层级参数。
- 深度由数据决定;设计稿推荐 2-3 层,不新增公开列数开关。
15 changes: 13 additions & 2 deletions tdesign-component/example/assets/api/tree-select_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
| multiple | bool | false | 是否允许选择多个叶子节点。 |
| multiple | bool | false | 是否允许选择多个叶子节点。 为 false 时,`value` 最多包含一条路径。 |
| onChanged | ValueChanged<List<List<Object?>>>? | - | 选中路径变化回调;为 null 时禁用。 |
| options | List<TTreeSelectOption> | - | 根选项。 |
| value | List<List<Object?>> | - | 受控选中路径。 |
| value | List<List<Object?>> | - | 受控选中路径。 每一项应为从根到叶子的完整 `TTreeSelectOption.value` 路径。 暂时无法在 `options` 中解析到叶子的路径不会显示选中态。 组件会回退到首个可用分支。单选模式最多传入一条,多选模式可传入多条且不得重复。 |


### TTreeSelectOption
#### 默认构造方法

| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| children | List<TTreeSelectOption> | const [] | 子选项。 |
| disabled | bool | false | 是否禁用。 |
| label | String | - | 展示文案。 |
| value | Object? | - | 业务值;同一层级的选项必须保持唯一。 值可为 null,但同一层级最多只能有一个 null 值。 |
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';

/// 在已配置 TDesign 主题的应用中使用 `TreeSelectMultipleExample()`。
class TreeSelectMultipleExample extends StatefulWidget {
const TreeSelectMultipleExample({super.key});

@override
State<TreeSelectMultipleExample> createState() =>
_TreeSelectMultipleExampleState();
}

class _TreeSelectMultipleExampleState extends State<TreeSelectMultipleExample> {
static const _options = [
TTreeSelectOption(
label: '甘肃省',
value: 'gansu',
children: [TTreeSelectOption(label: '兰州市', value: 'lanzhou')],
),
TTreeSelectOption(
label: '广东省',
value: 'guangdong',
children: [
TTreeSelectOption(label: '汕头市', value: 'shantou'),
TTreeSelectOption(label: '汕尾市', value: 'shanwei'),
TTreeSelectOption(label: '韶关市', value: 'shaoguan'),
TTreeSelectOption(label: '深圳市', value: 'shenzhen'),
TTreeSelectOption(label: '阳江市', value: 'yangjiang'),
TTreeSelectOption(label: '云浮市', value: 'yunfu'),
],
),
TTreeSelectOption(
label: '贵州省',
value: 'guizhou',
children: [TTreeSelectOption(label: '贵阳市', value: 'guiyang')],
),
TTreeSelectOption(
label: '海南省',
value: 'hainan',
children: [TTreeSelectOption(label: '海口市', value: 'haikou')],
),
TTreeSelectOption(
label: '河北省',
value: 'hebei',
children: [TTreeSelectOption(label: '石家庄市', value: 'shijiazhuang')],
),
TTreeSelectOption(
label: '黑龙江省',
value: 'heilongjiang',
children: [TTreeSelectOption(label: '哈尔滨市', value: 'haerbin')],
),
];

List<List<Object?>> _value = const [
['guangdong', 'shanwei'],
];

@override
Widget build(BuildContext context) => TTreeSelect(
key: const ValueKey('tree-select-multiple'),
options: _options,
value: _value,
multiple: true,
onChanged: (value) => setState(() => _value = value),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';

/// 在已配置 TDesign 主题的应用中使用 `TreeSelectSingleExample()`。
class TreeSelectSingleExample extends StatefulWidget {
const TreeSelectSingleExample({super.key});

@override
State<TreeSelectSingleExample> createState() =>
_TreeSelectSingleExampleState();
}

class _TreeSelectSingleExampleState extends State<TreeSelectSingleExample> {
static const _options = [
TTreeSelectOption(
label: '甘肃省',
value: 'gansu',
children: [TTreeSelectOption(label: '兰州市', value: 'lanzhou')],
),
TTreeSelectOption(
label: '广东省',
value: 'guangdong',
children: [
TTreeSelectOption(label: '汕头市', value: 'shantou'),
TTreeSelectOption(label: '汕尾市', value: 'shanwei'),
TTreeSelectOption(label: '韶关市', value: 'shaoguan'),
TTreeSelectOption(label: '深圳市', value: 'shenzhen'),
TTreeSelectOption(label: '阳江市', value: 'yangjiang'),
TTreeSelectOption(label: '云浮市', value: 'yunfu'),
],
),
TTreeSelectOption(
label: '贵州省',
value: 'guizhou',
children: [TTreeSelectOption(label: '贵阳市', value: 'guiyang')],
),
TTreeSelectOption(
label: '海南省',
value: 'hainan',
children: [TTreeSelectOption(label: '海口市', value: 'haikou')],
),
TTreeSelectOption(
label: '河北省',
value: 'hebei',
children: [TTreeSelectOption(label: '石家庄市', value: 'shijiazhuang')],
),
TTreeSelectOption(
label: '黑龙江省',
value: 'heilongjiang',
children: [TTreeSelectOption(label: '哈尔滨市', value: 'haerbin')],
),
];

List<List<Object?>> _value = const [
['guangdong', 'shanwei'],
];

@override
Widget build(BuildContext context) => TTreeSelect(
key: const ValueKey('tree-select-single'),
options: _options,
value: _value,
onChanged: (value) => setState(() => _value = value),
);
}
Loading
Loading