Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
| 名称 | 返回类型 | 参数 | 说明 |
| --- | --- | --- | --- |
| showDatePicker | | required null context, String? title, double? titleHeight, Color? titleDividerColor, required DatePickerCallback? onConfirm, DatePickerCallback? onCancel, DatePickerCallback? onChange, Function(int wheelIndex, int index)? onSelectedItemChanged, String? leftText, TextStyle? leftTextStyle, TextStyle? centerTextStyle, String? rightText, TextStyle? rightTextStyle, EdgeInsets? padding, double? leftPadding, double? topPadding, double? rightPadding, double? topRadius, Color? backgroundColor, Widget? customSelectWidget, bool useYear, bool useMonth, bool useDay, bool useHour, bool useMinute, bool useSecond, bool useWeekDay, List<int> dateStart, List<int>? dateEnd, List<int>? initialDate, List<int> Function(DateTypeKey key, List<int> nums)? filterItems, double pickerHeight, int pickerItemCount, bool isTimeUnit, ItemBuilderType? itemBuilder, Color? barrierColor, Duration duration, | 显示时间选择器 |
| showMultiLinkedPicker | | required null context, String? title, required MultiPickerCallback? onConfirm, MultiPickerCallback? onCancel, required List initialData, required Map data, required int columnNum, double pickerHeight, int pickerItemCount, Widget? customSelectWidget, String? rightText, String? leftText, TextStyle? leftTextStyle, TextStyle? centerTextStyle, TextStyle? rightTextStyle, double? titleHeight, double? topPadding, double? leftPadding, double? rightPadding, Color? titleDividerColor, Color? backgroundColor, double? topRadius, EdgeInsets? padding, ItemBuilderType? itemBuilder, bool keepSameSelection, Color? barrierColor, Duration duration, | 显示多级联动选择器 |
| showMultiLinkedPicker | | required null context, String? title, required MultiPickerCallback? onConfirm, MultiPickerCallback? onCancel, required List initialData, required Map data, required int columnNum, double pickerHeight, int pickerItemCount, Widget? customSelectWidget, String? rightText, String? leftText, TextStyle? leftTextStyle, TextStyle? centerTextStyle, TextStyle? rightTextStyle, double? titleHeight, double? topPadding, double? leftPadding, double? rightPadding, Color? titleDividerColor, Color? backgroundColor, double? topRadius, EdgeInsets? padding, ItemBuilderType? itemBuilder, bool keepSameSelection, Color? barrierColor, LinkedPickerColumnChangedCallback? onColumnChanged, Duration duration, | 显示多级联动选择器 |
| showMultiPicker | | required null context, String? title, required MultiPickerCallback? onConfirm, MultiPickerCallback? onCancel, required List<List<String>> data, double pickerHeight, int pickerItemCount, List<int>? initialIndexes, String? rightText, String? leftText, TextStyle? leftTextStyle, TextStyle? centerTextStyle, TextStyle? rightTextStyle, double? titleHeight, double? topPadding, double? leftPadding, double? rightPadding, Color? titleDividerColor, Color? backgroundColor, double? topRadius, EdgeInsets? padding, Widget? customSelectWidget, ItemBuilderType? itemBuilder, Duration duration, Color? barrierColor, | 显示多级选择器 |

```
Expand Down
3 changes: 2 additions & 1 deletion tdesign-component/example/assets/api/picker_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
| leftTextStyle | TextStyle? | - | 自定义左侧文案样式 |
| onCancel | MultiPickerCallback? | - | 选择器取消按钮回调 |
| onChange | MultiPickerCallback? | - | todo 选择器数据改变时回调 |
| onColumnChanged | LinkedPickerColumnChangedCallback? | - | 列选项变化时的回调,用于动态加载下一列数据 |
| onConfirm | MultiPickerCallback? | - | 选择器确认按钮回调 |
| padding | EdgeInsets? | - | 适配padding |
| pickerHeight | double | 200 | |
Expand Down Expand Up @@ -91,5 +92,5 @@
| 名称 | 返回类型 | 参数 | 说明 |
| --- | --- | --- | --- |
| showDatePicker | | required null context, String? title, double? titleHeight, Color? titleDividerColor, required DatePickerCallback? onConfirm, DatePickerCallback? onCancel, DatePickerCallback? onChange, Function(int wheelIndex, int index)? onSelectedItemChanged, String? leftText, TextStyle? leftTextStyle, TextStyle? centerTextStyle, String? rightText, TextStyle? rightTextStyle, EdgeInsets? padding, double? leftPadding, double? topPadding, double? rightPadding, double? topRadius, Color? backgroundColor, Widget? customSelectWidget, bool useYear, bool useMonth, bool useDay, bool useHour, bool useMinute, bool useSecond, bool useWeekDay, List<int> dateStart, List<int>? dateEnd, List<int>? initialDate, List<int> Function(DateTypeKey key, List<int> nums)? filterItems, double pickerHeight, int pickerItemCount, bool isTimeUnit, ItemBuilderType? itemBuilder, Color? barrierColor, Duration duration, | 显示时间选择器 |
| showMultiLinkedPicker | | required null context, String? title, required MultiPickerCallback? onConfirm, MultiPickerCallback? onCancel, required List initialData, required Map data, required int columnNum, double pickerHeight, int pickerItemCount, Widget? customSelectWidget, String? rightText, String? leftText, TextStyle? leftTextStyle, TextStyle? centerTextStyle, TextStyle? rightTextStyle, double? titleHeight, double? topPadding, double? leftPadding, double? rightPadding, Color? titleDividerColor, Color? backgroundColor, double? topRadius, EdgeInsets? padding, ItemBuilderType? itemBuilder, bool keepSameSelection, Color? barrierColor, Duration duration, | 显示多级联动选择器 |
| showMultiLinkedPicker | | required null context, String? title, required MultiPickerCallback? onConfirm, MultiPickerCallback? onCancel, required List initialData, required Map data, required int columnNum, double pickerHeight, int pickerItemCount, Widget? customSelectWidget, String? rightText, String? leftText, TextStyle? leftTextStyle, TextStyle? centerTextStyle, TextStyle? rightTextStyle, double? titleHeight, double? topPadding, double? leftPadding, double? rightPadding, Color? titleDividerColor, Color? backgroundColor, double? topRadius, EdgeInsets? padding, ItemBuilderType? itemBuilder, bool keepSameSelection, Color? barrierColor, LinkedPickerColumnChangedCallback? onColumnChanged, Duration duration, | 显示多级联动选择器 |
| showMultiPicker | | required null context, String? title, required MultiPickerCallback? onConfirm, MultiPickerCallback? onCancel, required List<List<String>> data, double pickerHeight, int pickerItemCount, List<int>? initialIndexes, String? rightText, String? leftText, TextStyle? leftTextStyle, TextStyle? centerTextStyle, TextStyle? rightTextStyle, double? titleHeight, double? topPadding, double? leftPadding, double? rightPadding, Color? titleDividerColor, Color? backgroundColor, double? topRadius, EdgeInsets? padding, Widget? customSelectWidget, ItemBuilderType? itemBuilder, Duration duration, Color? barrierColor, | 显示多级选择器 |
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

Widget buildDynamicLinkedPicker(BuildContext context) {
return TCell(
title: '动态加载(onColumnChanged)',
note: selected_3.isEmpty ? '请选择' : selected_3,
arrow: true,
onClick: (click) {
TPicker.showMultiLinkedPicker(
context,
title: '选择地区(动态加载)',
onConfirm: (selected) {
setState(() {
selected_3 = selected.join(' ');
});
Navigator.of(context).pop();
},
// data 传空 Map,完全由 onColumnChanged 提供数据
data: {
'广东省': {},
'浙江省': {},
},
columnNum: 3,
initialData: ['广东省', '', ''],
onColumnChanged: _fetchNextColumnData,
);
},
);
}
55 changes: 55 additions & 0 deletions tdesign-component/example/lib/page/t_picker_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ class _TPickerPageState extends State<TPickerPage> {
ExampleItem(
desc: '自定义left/right text', builder: buildCustomLeftRightText),
ExampleItem(desc: '级联选择保持下一级选项', builder: buildKeepMultiArea),
ExampleItem(
desc: '动态加载下一列数据(onColumnChanged)',
builder: buildDynamicLinkedPicker),
],
);
}
Expand Down Expand Up @@ -428,4 +431,56 @@ class _TPickerPageState extends State<TPickerPage> {
},
);
}

/// 模拟异步数据源:根据当前列选中值返回下一列数据
Future<List> _fetchNextColumnData(int columnIndex, List selectedData) async {
// 模拟网络延迟 300ms
await Future.delayed(const Duration(milliseconds: 300));
if (columnIndex == 0) {
final province = selectedData[0]?.toString() ?? '';
if (province == '广东省') {
return ['深圳市', '广州市', '佛山市'];
} else if (province == '浙江省') {
return ['杭州市', '宁波市', '温州市'];
}
return ['(无数据)'];
} else if (columnIndex == 1) {
final city = selectedData[1]?.toString() ?? '';
if (city == '深圳市') return ['南山区', '宝安区', '罗湖区'];
if (city == '广州市') return ['天河区', '越秀区', '白云区'];
if (city == '杭州市') return ['西湖区', '余杭区', '萧山区'];
if (city == '宁波市') return ['江东区', '北仑区', '奉化市'];
return ['(无数据)'];
}
return [];
}

@Demo(group: 'picker')
Widget buildDynamicLinkedPicker(BuildContext context) {
return TCell(
title: '动态加载(onColumnChanged)',
note: selected_3.isEmpty ? '请选择' : selected_3,
arrow: true,
onClick: (click) {
TPicker.showMultiLinkedPicker(
context,
title: '选择地区(动态加载)',
onConfirm: (selected) {
setState(() {
selected_3 = selected.join(' ');
});
Navigator.of(context).pop();
},
// data 传空 Map,完全由 onColumnChanged 提供数据
data: {
'广东省': {},
'浙江省': {},
},
columnNum: 3,
initialData: ['广东省', '', ''],
onColumnChanged: _fetchNextColumnData,
);
Comment on lines +474 to +482

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

示例里把 data 的下一层设为 {}(空 Map),但当前 MultiLinkedPickerModel 初始化第 1/2 列时会从 Map keys 取数据;空 keys 会导致该列数据为空,从而在创建 FixedExtentScrollController 时出现空数据列的异常/不可用。建议示例中至少提供占位数据(例如 [''])或调整组件实现:当下一层为空时回退到 placeData,并可在 initState 根据 initialData 主动触发 onColumnChanged 预加载下一列。

Copilot uses AI. Check for mistakes.
},
);
}
}
123 changes: 120 additions & 3 deletions tdesign-component/lib/src/components/picker/t_multi_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ import 'no_wave_behavior.dart';

typedef MultiPickerCallback = void Function(List selected);

/// 列选项变化时的回调类型
/// [columnIndex] 发生变化的列索引
/// [selectedData] 当前各列已选中的数据
/// 返回值:columnIndex+1 列需要展示的新数据列表
typedef LinkedPickerColumnChangedCallback = Future<List> Function(
int columnIndex,
List selectedData,
);

/// 项之间无联动的多项选择器
class TMultiPicker extends StatelessWidget {
/// 选择器标题
Expand Down Expand Up @@ -424,6 +433,12 @@ class TMultiLinkedPicker extends StatefulWidget {
/// 是否显示头部内容
final bool header;

/// 列选项变化时的回调,用于动态加载下一列数据
///
/// 当第 [columnIndex] 列选项发生变化时,调用此回调获取第 [columnIndex]+1 列的数据。
/// 若不提供,则沿用 [data] Map 中的数据(向后兼容)。
final LinkedPickerColumnChangedCallback? onColumnChanged;

const TMultiLinkedPicker({
this.title,
required this.onConfirm,
Expand Down Expand Up @@ -452,6 +467,7 @@ class TMultiLinkedPicker extends StatefulWidget {
this.itemBuilder,
this.keepSameSelection = false,
this.header = true,
this.onColumnChanged,
Key? key,
}) : super(key: key);

Expand Down Expand Up @@ -598,11 +614,14 @@ class _TMultiLinkedPickerState extends State<TMultiLinkedPicker> {
physics: const FixedExtentScrollPhysics(),
onSelectedItemChanged: (index) {
if (index >= 0 && index < model.presentData[position].length) {
final hasCallback = widget.onColumnChanged != null &&
position < widget.columnNum - 1;
setState(() {
model.refreshPresentDataAndController(
position,
index,
false,
cascadeNext: !hasCallback,
);
if (index >= model.presentData[position].length - 5 &&
model.hasMoreData[position]) {
Expand All @@ -621,12 +640,33 @@ class _TMultiLinkedPickerState extends State<TMultiLinkedPicker> {
pickerHeight =
pickerHeight - Random().nextDouble() / 100000000;
});

if (hasCallback) {
_loadNextColumnData(position);
}
}
},
childDelegate: ListWheelChildBuilderDelegate(
childCount: model.presentData[position].length +
(model.hasMoreData[position] ? 1 : 0),
builder: (context, index) {
// 展示加载中占位
if (model.isLoading[position] &&
index == 0 &&
model.presentData[position].length == 1 &&
model.presentData[position].first ==
MultiLinkedPickerModel.placeData) {
return Container(
alignment: Alignment.center,
height: pickerHeight / widget.pickerItemCount,
child: Text(
context.resource.loadingWithPoint,
style: TextStyle(
color: TTheme.of(context).textColorPlaceholder,
),
),
);
}
if (index >= model.presentData[position].length) {
// 加载更多指示器
return Container(
Expand Down Expand Up @@ -665,6 +705,30 @@ class _TMultiLinkedPickerState extends State<TMultiLinkedPicker> {
);
}

/// 调用 [widget.onColumnChanged] 异步加载下一列数据
Future<void> _loadNextColumnData(int columnIndex) async {
final nextColumn = columnIndex + 1;
setState(() {
model.resetColumnsAfter(columnIndex);
model.setLoading(nextColumn, true);
});
try {
final newData =
await widget.onColumnChanged!(columnIndex, model.selectedData);
if (!mounted) return;
setState(() {
model.updateColumnData(nextColumn, newData);
model.setLoading(nextColumn, false);
});
Comment on lines +709 to +722

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_loadNextColumnData 没有处理用户快速滚动导致的并发请求乱序:先发起的 onColumnChanged 可能在后发起的之后返回,从而用过期数据覆盖当前选择对应的列数据/加载状态。建议为每次请求引入递增 requestId/token,并在 await 返回后校验 token 或当前 selectedData 是否仍匹配,再决定是否 updateColumnData/setLoading。

Copilot uses AI. Check for mistakes.
} catch (_) {
if (!mounted) return;
setState(() {
model.updateColumnData(nextColumn, []);
model.setLoading(nextColumn, false);
});
}
}

Widget _buildHeader(BuildContext context) {
final padding = TTheme.of(context).spacer16;

Expand Down Expand Up @@ -781,6 +845,9 @@ class MultiLinkedPickerModel {
/// 每列的总数据量
late List<int> totalCounts;

/// 每列是否正在异步加载数据
late List<bool> isLoading;

MultiLinkedPickerModel({
required this.data,
required this.columnNum,
Expand All @@ -792,6 +859,7 @@ class MultiLinkedPickerModel {
currentPages = List.generate(columnNum, (_) => 0);
hasMoreData = List.generate(columnNum, (_) => true);
totalCounts = List.generate(columnNum, (_) => 0);
isLoading = List.generate(columnNum, (_) => false);
for (var i = 0; i < columnNum; ++i) {
if (i >= initialData.length) {
selectedData.add('');
Expand Down Expand Up @@ -892,11 +960,14 @@ class MultiLinkedPickerModel {
/// [position] 变动的列
/// [selectedIndex] 对应选中的index
/// [jump] 是否需要jumpToItem
/// [cascadeNext] 是否自动级联更新后续列(默认 true);
/// 传 false 时仅更新当前列的选中状态,后续列由外部的 [LinkedPickerColumnChangedCallback] 负责
void refreshPresentDataAndController(
int position,
int selectedIndex,
bool jump,
) {
bool jump, {
bool cascadeNext = true,
}) {
// 严格的边界检查
if (position >= presentData.length ||
selectedIndex >= presentData[position].length ||
Expand All @@ -916,7 +987,7 @@ class MultiLinkedPickerModel {
hasMoreData[position]) {
loadMoreData(position);
}
if (position < columnNum - 1) {
if (cascadeNext && position < columnNum - 1) {
List nextColumnData;
if (presentData[position].length == 1 &&
presentData[position].first == placeData) {
Expand All @@ -939,4 +1010,50 @@ class MultiLinkedPickerModel {
refreshPresentDataAndController(position + 1, 0, true);
}
}

/// 将 [columnIndex] 之后的所有列重置为占位状态
/// 重置后 hasMoreData 设为 false,避免在动态加载期间额外展示"加载更多"指示器
void resetColumnsAfter(int columnIndex) {
for (var i = columnIndex + 1; i < columnNum; i++) {
while (presentData.length <= i) {
presentData.add([placeData]);
}
presentData[i] = [placeData];
currentPages[i] = 0;
hasMoreData[i] = false;
if (i < controllers.length) {
controllers[i].jumpToItem(0);
}
}
Comment on lines +1016 to +1027

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resetColumnsAfter 只重置了 presentData/controller/hasMoreData,但没有同步重置后续列的 selectedData/selectedIndexes;当用户改变前一列并触发动态加载时,确认/回调仍可能携带旧的后续列选中值,导致结果与 UI 不一致。建议在重置后续列时同时把 selectedData[i] 置为占位值、selectedIndexes[i] 置 0。

Copilot uses AI. Check for mistakes.
}

/// 将 [columnIndex] 列的展示数据更新为 [newData]
///
/// 若 [newData] 为空,则使用占位符 [placeData]。
/// 同时将该列 controller 重置到第 0 项。
void updateColumnData(int columnIndex, List newData) {
while (presentData.length <= columnIndex) {
presentData.add([placeData]);
}
final data = newData.isEmpty ? [placeData] : newData;
presentData[columnIndex] = data;
currentPages[columnIndex] = 0;
hasMoreData[columnIndex] = false;
while (controllers.length <= columnIndex) {
controllers.add(FixedExtentScrollController(initialItem: 0));
}
controllers[columnIndex].jumpToItem(0);
if (columnIndex < selectedData.length) {
selectedData[columnIndex] =
data.isNotEmpty ? data.first : placeData;
selectedIndexes[columnIndex] = 0;
}
}

/// 设置 [columnIndex] 列的加载状态
void setLoading(int columnIndex, bool loading) {
if (columnIndex >= 0 && columnIndex < columnNum) {
isLoading[columnIndex] = loading;
}
}
}
6 changes: 6 additions & 0 deletions tdesign-component/lib/src/components/picker/t_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ class TPicker {
bool keepSameSelection = false,
Color? barrierColor,

/// 列选项变化时的回调,用于动态加载下一列数据
/// 当第 [columnIndex] 列选项变化时调用,返回第 [columnIndex]+1 列的数据
/// 若不提供,则沿用 [data] Map 中的数据(向后兼容)
LinkedPickerColumnChangedCallback? onColumnChanged,

/// todo 未传参
Duration duration = const Duration(milliseconds: 100),
}) {
Expand Down Expand Up @@ -231,6 +236,7 @@ class TPicker {
itemBuilder: itemBuilder,
customSelectWidget: customSelectWidget,
keepSameSelection: keepSameSelection,
onColumnChanged: onColumnChanged,
// itemDistanceCalculator: itemDistanceCalculator,
);
},
Expand Down
Loading
Loading