diff --git a/.github/workflows/typos-config.toml b/.github/workflows/typos-config.toml index d9918c1e2..8462df8d4 100644 --- a/.github/workflows/typos-config.toml +++ b/.github/workflows/typos-config.toml @@ -1,6 +1,8 @@ default.check-filename = true [default.extend-words] +"BA" = "BA" +"PA" = "PA" [files] extend-exclude = ["project.pbxproj","aop_flutter_sdk.patch"] diff --git a/.gitignore b/.gitignore index 8044543c4..17a2e1078 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,7 @@ build/ !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 +.codebuddy/ .fvm/ /tdesign-component/local_dependency_override.yaml diff --git a/tdesign-component/demo_tool/all_build.sh b/tdesign-component/demo_tool/all_build.sh index b9264572d..fc1d69df5 100644 --- a/tdesign-component/demo_tool/all_build.sh +++ b/tdesign-component/demo_tool/all_build.sh @@ -46,14 +46,12 @@ dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/compo # checkbox dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/checkbox" --name TCheckbox,TCheckboxGroup --folder-name checkbox --output "$PARENT_DIR/example/assets/api/" --only-api -# date_picker -dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/picker" --name TPicker,TDatePicker --folder-name date-time-picker --output "$PARENT_DIR/example/assets/api/" --only-api +# picker +dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/picker" --name TPicker,TPickerOption,TPickerValue,TPickerScrollPhysics --folder-name picker --output "$PARENT_DIR/example/assets/api/" --only-api # form dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/form" --name TForm,TFormItem,TFormItemType,TFormValidation --folder-name form --output "$PARENT_DIR/example/assets/api/" --only-api # input dart run tdesign_flutter_tools:main generate --file "$PARENT_DIR/lib/src/components/input/t_input.dart" --name TInput, TInputSpacer --folder-name input --output "$PARENT_DIR/example/assets/api/" --only-api -# picker -dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/picker" --name TPicker,TMultiPicker,TMultiLinkedPicker,MultiLinkedPickerModel --folder-name picker --output "$PARENT_DIR/example/assets/api/" --only-api # radio dart run tdesign_flutter_tools:main generate --file "$PARENT_DIR/lib/src/components/radio/t_radio.dart" --name TRadioStyle,TRadio,TRadioGroup --folder-name radio --output "$PARENT_DIR/example/assets/api/" --only-api --get-comments # rate diff --git a/tdesign-component/example/assets/api/date-time-picker_api.md b/tdesign-component/example/assets/api/date-time-picker_api.md index b5b57b933..cefa8d54c 100644 --- a/tdesign-component/example/assets/api/date-time-picker_api.md +++ b/tdesign-component/example/assets/api/date-time-picker_api.md @@ -1,46 +1,15 @@ ## API ### TPicker - -#### 静态方法 - -| 名称 | 返回类型 | 参数 | 说明 | -| --- | --- | --- | --- | -| 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 dateStart, List? dateEnd, List? initialDate, List Function(DateTypeKey key, List 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, | 显示多级联动选择器 | -| showMultiPicker | | required null context, String? title, required MultiPickerCallback? onConfirm, MultiPickerCallback? onCancel, required List> data, double pickerHeight, int pickerItemCount, List? 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, | 显示多级选择器 | - -``` -``` - -### TDatePicker #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| backgroundColor | Color? | - | 背景颜色 | -| centerTextStyle | TextStyle? | - | 自定义中间文案样式 | -| customSelectWidget | Widget? | - | 自定义选择框样式 | -| header | bool | true | 是否显示头部内容 | -| isTimeUnit | bool? | - | 是否时间显示 | -| itemBuilder | ItemBuilderType? | - | 自定义item构建 | -| itemDistanceCalculator | ItemDistanceCalculator? | - | 根据距离计算字体颜色、透明度、粗细 | +| disabled | bool | false | 是否禁用整个选择器(禁止滚动和操作),默认 false | +| height | double | 200 | 视窗高度,默认 200 | +| initialValue | List? | - | 初始选中值列表(按 value 匹配) | +| itemCount | int | 5 | 每屏显示 item 数,默认 5 | +| items | dynamic | - | 数据源(必填) | | key | | - | | -| leftPadding | double? | - | 左边填充 | -| leftText | String? | - | 左侧按钮文案 | -| leftTextStyle | TextStyle? | - | 自定义左侧文案样式 | -| model | DatePickerModel | - | 数据模型 | -| onCancel | DatePickerCallback? | - | 选择器取消按钮回调 | -| onChange | DatePickerCallback? | - | 选择器值改变回调 | -| onConfirm | DatePickerCallback? | - | 选择器确认按钮回调 | -| onSelectedItemChanged | void Function(int wheelIndex, int index)? | - | 选择器选中项改变回调 | -| padding | EdgeInsets? | - | 适配padding | -| pickerHeight | double | 200 | 选择器List的视窗高度,默认200 | -| pickerItemCount | int | 5 | 选择器List视窗中item个数,pickerHeight / pickerItemCount,即item高度 | -| rightPadding | double? | - | 右边填充 | -| rightText | String? | - | 右侧按钮文案 | -| rightTextStyle | TextStyle? | - | 自定义右侧文案样式 | -| title | String? | - | 选择器标题 | -| titleDividerColor | Color? | - | 标题分割线颜色 | -| titleHeight | double? | - | 标题高度 | -| topPadding | double? | - | 顶部填充 | -| topRadius | double? | - | 顶部圆角 | +| onChange | void Function(TPickerValue)? | - | 值改变回调 | +| onLoad | void Function(TPickerLoadEvent)? | - | 接近底部时加载回调 | +| preloadThreshold | int | 5 | 预加载阈值(距底部剩余 N 项时触发),默认 5 | diff --git a/tdesign-component/example/assets/api/picker_api.md b/tdesign-component/example/assets/api/picker_api.md index 45c53f352..08ab5c99a 100644 --- a/tdesign-component/example/assets/api/picker_api.md +++ b/tdesign-component/example/assets/api/picker_api.md @@ -1,95 +1,48 @@ ## API -### TMultiPicker +### TPickerOption #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| backgroundColor | Color? | - | 背景颜色 | -| centerTextStyle | TextStyle? | - | 自定义中间文案样式 | -| customSelectWidget | Widget? | - | 自定义选择框样式 | -| data | Map | - | 总的数据 | -| header | bool | true | 是否显示头部内容 | -| initialIndexes | List? | - | 若为null表示全部从零开始 | -| itemBuilder | ItemBuilderType? | - | 自定义item构建 | -| itemDistanceCalculator | ItemDistanceCalculator? | - | 不同距离自选项计算策略 | -| key | | - | | -| leftPadding | double? | - | 左边填充 | -| leftText | String? | - | 左侧按钮文案 | -| leftTextStyle | TextStyle? | - | 自定义左侧文案样式 | -| onCancel | MultiPickerCallback? | - | 选择器取消按钮回调 | -| onChange | MultiPickerCallback? | - | todo 选择器数据改变时回调 | -| onConfirm | MultiPickerCallback? | - | 选择器确认按钮回调 | -| padding | EdgeInsets? | - | 适配padding | -| pickerHeight | double | 200 | | -| pickerItemCount | int | 5 | 选择器List视窗中item个数,pickerHeight / pickerItemCount,即item高度 | -| rightPadding | double? | - | 右边填充 | -| rightText | String? | - | 右侧按钮文案 | -| rightTextStyle | TextStyle? | - | 自定义右侧文案样式 | -| title | String? | - | 选择器标题 | -| titleDividerColor | Color? | - | 标题分割线颜色 | -| titleHeight | double? | - | 标题高度 | -| topPadding | double? | - | 顶部填充 | -| topRadius | double? | - | 顶部圆角 | +| disabled | bool | false | 是否禁用(不可选中/置灰显示),默认 false | +| label | String | - | 显示文字(可包含 emoji、单位、国际化等) | +| value | dynamic | - | 实际值(onChange 回调返回此字段) | ``` ``` -### TMultiLinkedPicker +### TPickerValue #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| backgroundColor | Color? | - | 背景颜色 | -| centerTextStyle | TextStyle? | - | 自定义中间文案样式 | -| columnNum | int | - | 总列数 | -| customSelectWidget | Widget? | - | 自定义选择框样式 | -| data | Map | - | 总的数据 | -| header | bool | true | 是否显示头部内容 | -| itemBuilder | ItemBuilderType? | - | 自定义item构建 | -| itemDistanceCalculator | ItemDistanceCalculator? | - | 不同距离自选项计算策略 | -| keepSameSelection | bool | false | 是否保留相同选项 | -| key | | - | | -| leftPadding | double? | - | 左边填充 | -| leftText | String? | - | 左侧按钮文案 | -| leftTextStyle | TextStyle? | - | 自定义左侧文案样式 | -| onCancel | MultiPickerCallback? | - | 选择器取消按钮回调 | -| onChange | MultiPickerCallback? | - | todo 选择器数据改变时回调 | -| onConfirm | MultiPickerCallback? | - | 选择器确认按钮回调 | -| padding | EdgeInsets? | - | 适配padding | -| pickerHeight | double | 200 | | -| pickerItemCount | int | 5 | 选择器List视窗中item个数,pickerHeight / pickerItemCount,即item高度 | -| rightPadding | double? | - | 右边填充 | -| rightText | String? | - | 右侧按钮文案 | -| rightTextStyle | TextStyle? | - | 自定义右侧文案样式 | -| selectedData | List | - | 选中数据 | -| title | String? | - | 选择器标题 | -| titleDividerColor | Color? | - | 标题分割线颜色 | -| titleHeight | double? | - | 标题高度 | -| topPadding | double? | - | 顶部填充 | -| topRadius | double? | - | 顶部圆角 | +| indexes | List | - | 每列在当前数据列表中的索引(便捷访问) | +| selectedOptions | List | - | 每列选中的完整 option(顺序对应列号) | ``` ``` -### MultiLinkedPickerModel +### TPicker #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| columnNum | int | - | 总列数 | -| data | Map | - | 总的数据 | -| initialData | | - | | -| keepSameSelection | bool | false | 是否保留相同选项 | +| disabled | bool | false | 是否禁用整个选择器(禁止滚动和操作),默认 false | +| height | double | 200 | 视窗高度,默认 200 | +| initialValue | List? | - | 初始选中值列表(按 value 匹配) | +| itemCount | int | 5 | 每屏显示 item 数,默认 5 | +| items | dynamic | - | 数据源(必填) | +| key | | - | | +| onChange | void Function(TPickerValue)? | - | 值改变回调 | +| onLoad | void Function(TPickerLoadEvent)? | - | 接近底部时加载回调 | +| preloadThreshold | int | 5 | 预加载阈值(距底部剩余 N 项时触发),默认 5 | ``` ``` -### TPicker - -#### 静态方法 +### TPickerScrollPhysics +#### 默认构造方法 -| 名称 | 返回类型 | 参数 | 说明 | +| 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| 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 dateStart, List? dateEnd, List? initialDate, List Function(DateTypeKey key, List 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, | 显示多级联动选择器 | -| showMultiPicker | | required null context, String? title, required MultiPickerCallback? onConfirm, MultiPickerCallback? onCancel, required List> data, double pickerHeight, int pickerItemCount, List? 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, | 显示多级选择器 | +| parent | | - | | diff --git a/tdesign-component/example/assets/code/datetimePicker._customItems.txt b/tdesign-component/example/assets/code/datetimePicker._customItems.txt deleted file mode 100644 index 712ee4f6b..000000000 --- a/tdesign-component/example/assets/code/datetimePicker._customItems.txt +++ /dev/null @@ -1,55 +0,0 @@ - - Widget _customItems(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_9.isEmpty ? '请选择' : selected_9, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_9 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')} ' - '${selected['hour'].toString().padLeft(2, '0')}:' - '${selected['minute'].toString().padLeft(2, '0')}:' - '${selected['second'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useHour: true, - useMinute: true, - useSecond: true, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - filterItems: (key, nums) { - if (key == DateTypeKey.minute) { - return [0, 15, 30]; - } - return nums; - }, - itemBuilder: (context, content, colIndex, index, - itemDistanceCalculator, distance) { - return colIndex == 5 - ? TText( - content, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: itemDistanceCalculator.calculateFontWeight( - context, distance), - fontSize: index % 2 == 0 ? 20 : 10, - color: index % 2 == 1 - ? TTheme.of(context).textColorPrimary - : TTheme.of(context).successColor6, - ), - ) - : null; - }, - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/datetimePicker._customItemsOnlyHour.txt b/tdesign-component/example/assets/code/datetimePicker._customItemsOnlyHour.txt deleted file mode 100644 index 254b45e0e..000000000 --- a/tdesign-component/example/assets/code/datetimePicker._customItemsOnlyHour.txt +++ /dev/null @@ -1,26 +0,0 @@ - - Widget _customItemsOnlyHour(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_9.isEmpty ? '请选择' : selected_9, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '只有时分', - onConfirm: (selected) { - Navigator.of(context).pop(); - }, - useYear: false, - useMonth: false, - useDay: false, - useSecond: false, - useHour: true, - useMinute: true, - dateStart: [2025, 1, 1, 20, 0, 0], - dateEnd: [2025, 1, 1, 23, 59, 0], - initialDate: [2025, 1, 1, 22, 46, 0], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/datetimePicker._customLimitTime.txt b/tdesign-component/example/assets/code/datetimePicker._customLimitTime.txt deleted file mode 100644 index 837e76475..000000000 --- a/tdesign-component/example/assets/code/datetimePicker._customLimitTime.txt +++ /dev/null @@ -1,31 +0,0 @@ - - Widget _customLimitTime(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_4.isEmpty ? '请选择' : selected_4, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_4 = '${selected['hour'].toString().padLeft(2, '0')}:' - '${selected['minute'].toString().padLeft(2, '0')}:' - '${selected['second'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useYear: false, - useMonth: false, - useDay: false, - useHour: true, - useMinute: true, - useSecond: true, - dateStart: [2023, 12, 31], - dateEnd: [2023, 12, 31, 4, 12, 20], - initialDate: [2023, 12, 31, 3, 02, 03], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/datetimePicker._customSelectWidget.txt b/tdesign-component/example/assets/code/datetimePicker._customSelectWidget.txt deleted file mode 100644 index c849f511b..000000000 --- a/tdesign-component/example/assets/code/datetimePicker._customSelectWidget.txt +++ /dev/null @@ -1,38 +0,0 @@ - - Widget _customSelectWidget(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_9, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_9 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')} ' - '${selected['hour'].toString().padLeft(2, '0')}:' - '${selected['minute'].toString().padLeft(2, '0')}:' - '${selected['second'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useHour: true, - useMinute: true, - useSecond: true, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - customSelectWidget: Container( - height: 40, - decoration: const BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.all(Radius.circular(6)), - ), - ), - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/datetimePicker._customStartTime.txt b/tdesign-component/example/assets/code/datetimePicker._customStartTime.txt deleted file mode 100644 index a18d19369..000000000 --- a/tdesign-component/example/assets/code/datetimePicker._customStartTime.txt +++ /dev/null @@ -1,34 +0,0 @@ - - Widget _customStartTime(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_5.isEmpty ? '请选择' : selected_5, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_5 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')} ' - '${selected['hour'].toString().padLeft(2, '0')}:' - '${selected['minute'].toString().padLeft(2, '0')}:' - '${selected['second'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useYear: true, - useMonth: true, - useDay: true, - useHour: true, - useMinute: true, - useSecond: true, - dateStart: [2012, 1, 15, 12, 28, 11], - dateEnd: [2012, 6, 15, 12, 48, 32], - initialDate: [2012, 1, 15, 13, 20], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/datetimePicker.buildAll.txt b/tdesign-component/example/assets/code/datetimePicker.buildAll.txt deleted file mode 100644 index 140e40c93..000000000 --- a/tdesign-component/example/assets/code/datetimePicker.buildAll.txt +++ /dev/null @@ -1,31 +0,0 @@ - - Widget buildAll(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_5.isEmpty ? '请选择' : selected_5, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_5 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')} ' - '${selected['hour'].toString().padLeft(2, '0')}:' - '${selected['minute'].toString().padLeft(2, '0')}:' - '${selected['second'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useHour: true, - useMinute: true, - useSecond: true, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/datetimePicker.buildHourMinuteSecond.txt b/tdesign-component/example/assets/code/datetimePicker.buildHourMinuteSecond.txt deleted file mode 100644 index b6a35a1d3..000000000 --- a/tdesign-component/example/assets/code/datetimePicker.buildHourMinuteSecond.txt +++ /dev/null @@ -1,31 +0,0 @@ - - Widget buildHourMinuteSecond(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_4.isEmpty ? '请选择' : selected_4, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_4 = '${selected['hour'].toString().padLeft(2, '0')}:' - '${selected['minute'].toString().padLeft(2, '0')}:' - '${selected['second'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useYear: false, - useMonth: false, - useDay: false, - useHour: true, - useMinute: true, - useSecond: true, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31, 4, 12, 20], - initialDate: [2023, 12, 31], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/datetimePicker.buildMonthDay.txt b/tdesign-component/example/assets/code/datetimePicker.buildMonthDay.txt deleted file mode 100644 index efa383911..000000000 --- a/tdesign-component/example/assets/code/datetimePicker.buildMonthDay.txt +++ /dev/null @@ -1,25 +0,0 @@ - - Widget buildMonthDay(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_3.isEmpty ? '请选择' : selected_3, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_3 = '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useYear: false, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/datetimePicker.buildWeekDay.txt b/tdesign-component/example/assets/code/datetimePicker.buildWeekDay.txt deleted file mode 100644 index 695c8a46c..000000000 --- a/tdesign-component/example/assets/code/datetimePicker.buildWeekDay.txt +++ /dev/null @@ -1,27 +0,0 @@ - - Widget buildWeekDay(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_6.isEmpty ? '请选择' : selected_6, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_6 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')} ' - '${weekDayList[selected['weekDay']! - 1]}'; - }); - Navigator.of(context).pop(); - }, - useWeekDay: true, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/datetimePicker.buildWithTitle.txt b/tdesign-component/example/assets/code/datetimePicker.buildWithTitle.txt deleted file mode 100644 index bce705a20..000000000 --- a/tdesign-component/example/assets/code/datetimePicker.buildWithTitle.txt +++ /dev/null @@ -1,25 +0,0 @@ - - Widget buildWithTitle(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_7.isEmpty ? '请选择' : selected_7, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_7 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/datetimePicker.buildWithoutHeader.txt b/tdesign-component/example/assets/code/datetimePicker.buildWithoutHeader.txt deleted file mode 100644 index d7e84cbcb..000000000 --- a/tdesign-component/example/assets/code/datetimePicker.buildWithoutHeader.txt +++ /dev/null @@ -1,21 +0,0 @@ - - Widget buildWithoutHeader(BuildContext context) { - return TDatePicker( - header: false, - model: DatePickerModel( - useYear: true, - useMonth: true, - useDay: true, - useHour: true, - useMinute: true, - useSecond: true, - useWeekDay: false, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - dateInitial: [2012, 1, 1], - ), - onChange: (selected) { - print('onChange ${selected}'); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/datetimePicker.buildWithoutTitle.txt b/tdesign-component/example/assets/code/datetimePicker.buildWithoutTitle.txt deleted file mode 100644 index 523b88b2d..000000000 --- a/tdesign-component/example/assets/code/datetimePicker.buildWithoutTitle.txt +++ /dev/null @@ -1,26 +0,0 @@ - - Widget buildWithoutTitle(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_8.isEmpty ? '请选择' : selected_8, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - // 不传或传空字符串、null,则不显示标题 - // title: '', - onConfirm: (selected) { - setState(() { - selected_8 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/datetimePicker.buildYearMonth.txt b/tdesign-component/example/assets/code/datetimePicker.buildYearMonth.txt deleted file mode 100644 index 084b774d6..000000000 --- a/tdesign-component/example/assets/code/datetimePicker.buildYearMonth.txt +++ /dev/null @@ -1,25 +0,0 @@ - - Widget buildYearMonth(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_2.isEmpty ? '请选择' : selected_2, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_2 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useDay: false, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/datetimePicker.buildYearMonthDay.txt b/tdesign-component/example/assets/code/datetimePicker.buildYearMonthDay.txt deleted file mode 100644 index f81a52ff9..000000000 --- a/tdesign-component/example/assets/code/datetimePicker.buildYearMonthDay.txt +++ /dev/null @@ -1,25 +0,0 @@ - - Widget buildYearMonthDay(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_1.isEmpty ? '请选择' : selected_1, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_1 = '${selected['year'].toString().padLeft(4, '0')}' - '-${selected['month'].toString().padLeft(2, '0')}' - '-${selected['day'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/form._buildCustomForm.txt b/tdesign-component/example/assets/code/form._buildCustomForm.txt index 2c5dec086..800e5aaf9 100644 --- a/tdesign-component/example/assets/code/form._buildCustomForm.txt +++ b/tdesign-component/example/assets/code/form._buildCustomForm.txt @@ -128,18 +128,17 @@ if (_formDisableState) { return; } - TPicker.showDatePicker(context, title: '选择时间', - onConfirm: (selected) { - setState(() { - _selected_1 = - '${selected['year'].toString().padLeft(4, '0')}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}'; - _formItemNotifier['birth']?.upDataForm(_selected_1); - }); - Navigator.of(context).pop(); - }, - dateStart: [1999, 01, 01], - dateEnd: [2050, 12, 31], - initialDate: [2012, 1, 1]); + _showDatePicker( + context, + initialDate: [2012, 1, 1], + onConfirm: (selected) { + setState(() { + _selected_1 = + '${selected[0].toString().padLeft(4, '0')}-${selected[1].toString().padLeft(2, '0')}-${selected[2].toString().padLeft(2, '0')}'; + _formItemNotifier['birth']?.upDataForm(_selected_1); + }); + }, + ); }, ), TFormItem( diff --git a/tdesign-component/example/assets/code/form._buildForm.txt b/tdesign-component/example/assets/code/form._buildForm.txt index f946a9f09..5a807549d 100644 --- a/tdesign-component/example/assets/code/form._buildForm.txt +++ b/tdesign-component/example/assets/code/form._buildForm.txt @@ -121,18 +121,17 @@ if (_formDisableState) { return; } - TPicker.showDatePicker(context, title: '选择时间', - onConfirm: (selected) { - setState(() { - _selected_1 = - '${selected['year'].toString().padLeft(4, '0')}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}'; - _formItemNotifier['birth']?.upDataForm(_selected_1); - }); - Navigator.of(context).pop(); - }, - dateStart: [1999, 01, 01], - dateEnd: [2050, 12, 31], - initialDate: [2012, 1, 1]); + _showDatePicker( + context, + initialDate: [2012, 1, 1], + onConfirm: (selected) { + setState(() { + _selected_1 = + '${selected[0].toString().padLeft(4, '0')}-${selected[1].toString().padLeft(2, '0')}-${selected[2].toString().padLeft(2, '0')}'; + _formItemNotifier['birth']?.upDataForm(_selected_1); + }); + }, + ); }, ), TFormItem( diff --git a/tdesign-component/example/assets/code/picker.buildArea.txt b/tdesign-component/example/assets/code/picker.buildArea.txt deleted file mode 100644 index 59bf53da1..000000000 --- a/tdesign-component/example/assets/code/picker.buildArea.txt +++ /dev/null @@ -1,22 +0,0 @@ - - Widget buildArea(BuildContext context) { - const title = '选择地区'; - return TCell( - title: title, - note: selected_1.isEmpty ? '请选择' : selected_1, - arrow: true, - onClick: (click) { - TPicker.showMultiPicker( - context, - title: title, - onConfirm: (selected) { - setState(() { - selected_1 = '${data_1[selected[0]]}'; - }); - Navigator.of(context).pop(); - }, - data: [data_1], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildAreaWithTitle.txt b/tdesign-component/example/assets/code/picker.buildAreaWithTitle.txt deleted file mode 100644 index 462699d64..000000000 --- a/tdesign-component/example/assets/code/picker.buildAreaWithTitle.txt +++ /dev/null @@ -1,22 +0,0 @@ - - Widget buildAreaWithTitle(BuildContext context) { - const title = '选择地区'; - return TCell( - title: title, - note: selected_4.isEmpty ? '请选择' : selected_4, - arrow: true, - onClick: (click) { - TPicker.showMultiPicker( - context, - title: '带标题选择器', - onConfirm: (selected) { - setState(() { - selected_4 = '${data_1[selected[0]]}'; - }); - Navigator.of(context).pop(); - }, - data: [data_1], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildAreaWithoutTitle.txt b/tdesign-component/example/assets/code/picker.buildAreaWithoutTitle.txt deleted file mode 100644 index 61f647de3..000000000 --- a/tdesign-component/example/assets/code/picker.buildAreaWithoutTitle.txt +++ /dev/null @@ -1,22 +0,0 @@ - - Widget buildAreaWithoutTitle(BuildContext context) { - return TCell( - title: '选择地区', - note: selected_5.isEmpty ? '请选择' : selected_5, - arrow: true, - onClick: (click) { - TPicker.showMultiPicker( - context, - // 不传或传空字符串、null,则不显示标题 - // title: '', - onConfirm: (selected) { - setState(() { - selected_5 = '${data_1[selected[0]]}'; - }); - Navigator.of(context).pop(); - }, - data: [data_1], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildCustomLeftRightText.txt b/tdesign-component/example/assets/code/picker.buildCustomLeftRightText.txt deleted file mode 100644 index b0f45ea84..000000000 --- a/tdesign-component/example/assets/code/picker.buildCustomLeftRightText.txt +++ /dev/null @@ -1,49 +0,0 @@ - - Widget buildCustomLeftRightText(BuildContext context) { - return TCellGroup( - cells: [ - TCell( - title: '基础选择器', - note: selected_5.isEmpty ? '请选择' : selected_5, - arrow: true, - onClick: (click) { - TPicker.showMultiPicker( - context, - leftText: '自定义取消', - rightText: '自定义确认', - title: '基础选择器', - onConfirm: (selected) { - setState(() { - selected_5 = '${data_1[selected[0]]}'; - }); - Navigator.of(context).pop(); - }, - data: [data_1], - ); - }, - ), - TCell( - title: '联动选择器', - note: selected_3.isEmpty ? '请选择' : selected_3, - arrow: true, - onClick: (click) { - TPicker.showMultiLinkedPicker( - context, - leftText: '自定义取消', - rightText: '自定义确认', - title: '联动选择器', - onConfirm: (selected) { - setState(() { - selected_3 = '${selected[0]} ${selected[1]} ${selected[2]}'; - }); - Navigator.of(context).pop(); - }, - data: data_3, - columnNum: 3, - initialData: ['浙江省', '杭州市', '西湖区'], - ); - }, - ) - ], - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildGlobalDisabled.txt b/tdesign-component/example/assets/code/picker.buildGlobalDisabled.txt new file mode 100644 index 000000000..546b570f6 --- /dev/null +++ b/tdesign-component/example/assets/code/picker.buildGlobalDisabled.txt @@ -0,0 +1,33 @@ + + Widget buildGlobalDisabled(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Switch( + value: globalDisabled, + onChanged: (v) => setState(() => globalDisabled = v), + ), + SizedBox(width: 8), + Text(globalDisabled ? '已禁用' : '已启用', + style: TextStyle( + fontSize: 14, + color: globalDisabled + ? TTheme.of(context).errorNormalColor + : TTheme.of(context).successNormalColor)), + ], + ), + SizedBox(height: 8), + _pickerCard( + context, + child: TPicker(items: cityData, initialValue: ['GZ'], + onChange: (v) => debugPrint('选中: $v'), + disabled: globalDisabled), + ), + SizedBox(height: 4), + Text('切换开关可控制整个选择器的禁用/启用状态', + style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), + ], + ); + } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildItemDisabled.txt b/tdesign-component/example/assets/code/picker.buildItemDisabled.txt new file mode 100644 index 000000000..f12c49886 --- /dev/null +++ b/tdesign-component/example/assets/code/picker.buildItemDisabled.txt @@ -0,0 +1,20 @@ + + Widget buildItemDisabled(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('选中: ${selectedItemDisabled.isEmpty ? "未选择" : selectedItemDisabled}', + style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + SizedBox(height: 4), + Text('提示: 标灰的选项不可选(第1列「保密」、第2列「A排1座/A排6座/A排7座/A排8座/A排12座」)', + style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), + SizedBox(height: 8), + _pickerCard( + context, + child: TPicker(items: itemDisabledData, initialValue: ['M', 'A5'], + onChange: (v) => setState(() => + selectedItemDisabled = '${v.labels.first} ${v.labels.last}')), + ), + ], + ); + } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildKeepMultiArea.txt b/tdesign-component/example/assets/code/picker.buildKeepMultiArea.txt deleted file mode 100644 index 900dfd5b9..000000000 --- a/tdesign-component/example/assets/code/picker.buildKeepMultiArea.txt +++ /dev/null @@ -1,24 +0,0 @@ - - Widget buildKeepMultiArea(BuildContext context) { - return TCell( - title: '选择地区', - note: selected_3.isEmpty ? '请选择' : selected_3, - arrow: true, - onClick: (click) { - TPicker.showMultiLinkedPicker( - context, - title: '选择地区', - onConfirm: (selected) { - setState(() { - selected_3 = '${selected[0]} ${selected[1]} ${selected[2]}'; - }); - Navigator.of(context).pop(); - }, - data: data_3, - columnNum: 3, - keepSameSelection: true, - initialData: ['广东省', '深圳市', '罗湖区'], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildLazyLoad.txt b/tdesign-component/example/assets/code/picker.buildLazyLoad.txt new file mode 100644 index 000000000..6ef031be9 --- /dev/null +++ b/tdesign-component/example/assets/code/picker.buildLazyLoad.txt @@ -0,0 +1,65 @@ + + Widget buildLazyLoad(BuildContext context) { + return StatefulBuilder( + builder: (ctx, setInner) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _lazyData.isEmpty + ? '暂无数据' + : '已加载 ${_lazyData.length} 条(滚动到底部自动加载更多)', + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary), + ), + const SizedBox(height: 8), + _pickerCard( + context, + child: TPicker( + items: [_lazyData], + preloadThreshold: 5, + onLoad: (e) async { + if (_isLoading) return; + setInner(() => _isLoading = true); + // 模拟网络请求延迟 1.5s + await Future.delayed(const Duration(milliseconds: 1500)); + final start = _lazyData.length + 1; + final more = [ + for (int i = start; i < start + 20; i++) + TPickerOption(label: '选项 $i', value: 'opt_$i'), + ]; + setInner(() { + _lazyData.addAll(more); + _isLoading = false; + }); + }, + onChange: (v) => setState(() => + selectedPreference = v.labels.first), + ), + ), + const SizedBox(height: 4), + if (_isLoading) + Row( + children: [ + SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2), + ), + const SizedBox(width: 6), + Text('正在加载更多数据...', + style: TextStyle( + fontSize: 12, + color: TTheme.of(context).textColorPlaceholder)), + ], + ) + else + Text('距底部 5 项时触发 onLoad,模拟 1.5s 网络延迟', + style: TextStyle( + fontSize: 12, + color: TTheme.of(context).textColorPlaceholder)), + ], + ); + }, + ); + } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildLinked.txt b/tdesign-component/example/assets/code/picker.buildLinked.txt new file mode 100644 index 000000000..c2e157ef5 --- /dev/null +++ b/tdesign-component/example/assets/code/picker.buildLinked.txt @@ -0,0 +1,16 @@ + + Widget buildLinked(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('选中地区: ${selectedLinked.isEmpty ? "未选择" : selectedLinked}', + style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + SizedBox(height: 8), + _pickerCard( + context, + child: TPicker(items: linkedData, initialValue: ['GD'], + onChange: (v) => setState(() => selectedLinked = v.labels.join(' / '))), + ), + ], + ); + } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildMultiArea.txt b/tdesign-component/example/assets/code/picker.buildMultiArea.txt deleted file mode 100644 index bb313046e..000000000 --- a/tdesign-component/example/assets/code/picker.buildMultiArea.txt +++ /dev/null @@ -1,24 +0,0 @@ - - Widget buildMultiArea(BuildContext context) { - const title = '选择地区'; - return TCell( - title: title, - note: selected_3.isEmpty ? '请选择' : selected_3, - arrow: true, - onClick: (click) { - TPicker.showMultiLinkedPicker( - context, - title: title, - onConfirm: (selected) { - setState(() { - selected_3 = '${selected[0]} ${selected[1]} ${selected[2]}'; - }); - Navigator.of(context).pop(); - }, - data: dataTest, - columnNum: 3, - initialData: ['浙江省', '杭州市', '西湖区'], - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildPopupLinked.txt b/tdesign-component/example/assets/code/picker.buildPopupLinked.txt new file mode 100644 index 000000000..d53d02520 --- /dev/null +++ b/tdesign-component/example/assets/code/picker.buildPopupLinked.txt @@ -0,0 +1,22 @@ + + Widget buildPopupLinked(BuildContext context) { + return TCell( + title: '弹窗-联动选择(省市区)', + note: selectedLinked.isEmpty ? '请选择' : selectedLinked, + arrow: true, + onClick: (_) { + _showPickerPopup( + context, + title: '请选择地区', + picker: TPicker( + items: linkedData, + initialValue: selectedLinked.isNotEmpty + ? selectedLinked.split(' / ') + : ['GD'], + onChange: (v) => setState(() => _popupLinkedTemp = v.labels.join(' / ')), + ), + onConfirm: () => setState(() => selectedLinked = _popupLinkedTemp), + ); + }, + ); + } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildPopupMultiColumn.txt b/tdesign-component/example/assets/code/picker.buildPopupMultiColumn.txt new file mode 100644 index 000000000..ccf87ffc5 --- /dev/null +++ b/tdesign-component/example/assets/code/picker.buildPopupMultiColumn.txt @@ -0,0 +1,23 @@ + + Widget buildPopupMultiColumn(BuildContext context) { + return TCell( + title: '弹窗-多列选择(性别/偏好)', + note: selectedPreference.isEmpty ? '请选择' : selectedPreference, + arrow: true, + onClick: (_) { + _showPickerPopup( + context, + title: '选择性别和偏好', + picker: TPicker( + items: preferenceData, + initialValue: selectedPreference.isNotEmpty + ? selectedPreference.split(' ') + : ['M', 'tech'], + onChange: (v) => setState(() => + _popupMultiColTemp = '${v.labels.first} ${v.labels.last}'), + ), + onConfirm: () => setState(() => selectedPreference = _popupMultiColTemp), + ); + }, + ); + } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildSingleColumn.txt b/tdesign-component/example/assets/code/picker.buildSingleColumn.txt new file mode 100644 index 000000000..2e9d02423 --- /dev/null +++ b/tdesign-component/example/assets/code/picker.buildSingleColumn.txt @@ -0,0 +1,16 @@ + + Widget buildSingleColumn(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('选中城市: ${selectedCity.isEmpty ? "未选择" : selectedCity}', + style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + SizedBox(height: 8), + _pickerCard( + context, + child: TPicker(items: cityData, + onChange: (v) => setState(() => selectedCity = v.labels.first)), + ), + ], + ); + } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildTime.txt b/tdesign-component/example/assets/code/picker.buildTime.txt deleted file mode 100644 index 2aa669c2f..000000000 --- a/tdesign-component/example/assets/code/picker.buildTime.txt +++ /dev/null @@ -1,24 +0,0 @@ - - Widget buildTime(BuildContext context) { - const title = '选择时间'; - return TCell( - title: title, - note: selected_2.isEmpty ? '请选择' : selected_2, - arrow: true, - onClick: (click) { - TPicker.showMultiPicker( - context, - title: title, - onConfirm: (selected) { - print('selected ${selected}'); - setState(() { - selected_2 = - '${data_2[0][selected[0]]} ${data_2[1][selected[1]]}'; - }); - Navigator.of(context).pop(); - }, - data: data_2, - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildTimeSelect.txt b/tdesign-component/example/assets/code/picker.buildTimeSelect.txt new file mode 100644 index 000000000..dd632325c --- /dev/null +++ b/tdesign-component/example/assets/code/picker.buildTimeSelect.txt @@ -0,0 +1,17 @@ + + Widget buildTimeSelect(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('选中时间: ${selectedTime.isEmpty ? "未选择" : selectedTime}', + style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + SizedBox(height: 8), + _pickerCard( + context, + child: TPicker(items: timeData, itemCount: 5, + onChange: (v) => setState(() => + selectedTime = '${v.values[0]}:${v.values[1].toString().padLeft(2, '0')}:${v.values[2].toString().padLeft(2, '0')}')), + ), + ], + ); + } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildWithoutHeader.txt b/tdesign-component/example/assets/code/picker.buildWithoutHeader.txt deleted file mode 100644 index dac1cbf48..000000000 --- a/tdesign-component/example/assets/code/picker.buildWithoutHeader.txt +++ /dev/null @@ -1,15 +0,0 @@ - - Widget buildWithoutHeader(BuildContext context) { - return TMultiPicker( - /// 不显示header内容 - header: false, - /// todo onChange - onConfirm: (selected) { - setState(() { - selected_5 = '${data_1[selected[0]]}'; - }); - Navigator.of(context).pop(); - }, - data: [data_1], - ); - } \ No newline at end of file diff --git a/tdesign-component/example/lib/config.dart b/tdesign-component/example/lib/config.dart index 137502e39..75684dcf7 100644 --- a/tdesign-component/example/lib/config.dart +++ b/tdesign-component/example/lib/config.dart @@ -20,7 +20,7 @@ import 'page/t_cascader_page.dart'; import 'page/t_cell_page.dart'; import 'page/t_checkbox_page.dart'; import 'page/t_collapse_page.dart'; -import 'page/t_date_picker_page.dart'; +import 'page/t_picker_page.dart'; import 'page/t_dialog_page.dart'; import 'page/t_divider_page.dart'; import 'page/t_drawer_page.dart'; @@ -157,11 +157,11 @@ Map> exampleMap = { name: 'checkbox', pageBuilder: _wrapInheritedTheme((context) => const TCheckboxPage())), ExamplePageModel( - text: 'DateTimePicker 时间选择器', - name: 'date-time-picker', - pageName: 'data_picker', + text: 'Picker 选择器', + name: 'picker', + pageName: 'picker', pageBuilder: - _wrapInheritedTheme((context) => const TDatePickerPage())), + _wrapInheritedTheme((context) => const TPickerPage())), ExamplePageModel( text: 'Form 表单', name: 'form', diff --git a/tdesign-component/example/lib/page/t_calendar_lunar_demo.dart b/tdesign-component/example/lib/page/t_calendar_lunar_demo.dart index 73f3aa032..9e9925d41 100644 --- a/tdesign-component/example/lib/page/t_calendar_lunar_demo.dart +++ b/tdesign-component/example/lib/page/t_calendar_lunar_demo.dart @@ -39,37 +39,26 @@ class _TCalendarLunarDemoState extends State { Row( children: [ Expanded( - child: TRadio( - id: 'solar', - title: '阳历模式', - radioStyle: TRadioStyle.circle, - showDivider: false, - enable: true, - checked: _dateType == TCalendarDateType.solar, - onChanged: (checked) { - if (checked) { - setState(() { - _dateType = TCalendarDateType.solar; - }); - } - }, + child: InkWell( + onTap: () => setState(() => _dateType = TCalendarDateType.solar), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: _dateType == TCalendarDateType.solar ? Colors.blue : Colors.grey[200], + borderRadius: BorderRadius.circular(4), + ), + child: Center(child: Text('阳历模式', style: TextStyle(color: _dateType == TCalendarDateType.solar ? Colors.white : Colors.black))), + ), ), ), Expanded( - child: TRadio( - id: 'lunar', - title: '农历模式', - radioStyle: TRadioStyle.circle, - showDivider: false, - enable: false, // 暂时禁用,因为需要实现数据源 - checked: _dateType == TCalendarDateType.lunar, - onChanged: (checked) { - if (checked) { - setState(() { - _dateType = TCalendarDateType.lunar; - }); - } - }, + child: InkWell( + onTap: () {}, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(4)), + child: Center(child: Text('农历模式', style: TextStyle(color: Colors.grey))), + ), ), ), ], @@ -78,12 +67,15 @@ class _TCalendarLunarDemoState extends State { TSwitch( enable: _dateType == TCalendarDateType.solar, isOn: _showLunarInfo, + size: TSwitchSize.large, onChanged: (value) { - setState(() { - _showLunarInfo = value; - }); + if (value != null && value != _showLunarInfo) { + setState(() { + _showLunarInfo = value; + }); + } + return true; }, - size: TSwitchSize.large, ), const SizedBox(height: 4), Text( diff --git a/tdesign-component/example/lib/page/t_calendar_lunar_example.dart b/tdesign-component/example/lib/page/t_calendar_lunar_example.dart index 43ff107b1..a0d1246e7 100644 --- a/tdesign-component/example/lib/page/t_calendar_lunar_example.dart +++ b/tdesign-component/example/lib/page/t_calendar_lunar_example.dart @@ -117,7 +117,7 @@ class _TCalendarLunarExampleState extends State { } final date = DateTime.fromMillisecondsSinceEpoch(_selectedDates.first); - final dataSource = _SimpleLunarDataSource(); + final dataSource = LunarDataSourceExample(); final lunarInfo = dataSource.getLunarInfo(date); return Container( diff --git a/tdesign-component/example/lib/page/t_date_picker_page.dart b/tdesign-component/example/lib/page/t_date_picker_page.dart deleted file mode 100644 index 63b29396c..000000000 --- a/tdesign-component/example/lib/page/t_date_picker_page.dart +++ /dev/null @@ -1,502 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:tdesign_flutter/tdesign_flutter.dart'; - -import '../../base/example_widget.dart'; -import '../annotation/demo.dart'; - -class TDatePickerPage extends StatefulWidget { - const TDatePickerPage({Key? key}) : super(key: key); - - @override - State createState() => _TDatePickerPageState(); -} - -class _TDatePickerPageState extends State { - String selected_1 = ''; - String selected_2 = ''; - String selected_3 = ''; - String selected_4 = ''; - String selected_5 = ''; - String selected_6 = ''; - String selected_7 = ''; - String selected_8 = ''; - String selected_9 = ''; - - final weekDayList = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return ExamplePage( - title: tTitle(), - desc: '用于选择一个时间点或者一个时间段。', - exampleCodeGroup: 'datetimePicker', - children: [ - ExampleModule( - title: '组件类型', - children: [ - ExampleItem(desc: '年月日选择器', builder: buildYearMonthDay), - ExampleItem(desc: '年月选择器', builder: buildYearMonth), - ExampleItem(desc: '月日选择器', builder: buildMonthDay), - ExampleItem(desc: '时分秒选择器', builder: buildHourMinuteSecond), - ExampleItem(desc: '年月日时分秒选择器', builder: buildAll), - ExampleItem(desc: '年月日带星期选择器', builder: buildWeekDay), - ], - ), - ExampleModule( - title: '组件样式', - children: [ - ExampleItem(desc: '是否带标题', builder: buildWithTitle), - ExampleItem(desc: '不带标题', builder: buildWithoutTitle), - ExampleItem(desc: '不使用弹窗、不带顶部内容', builder: buildWithoutHeader), - ], - ) - ], - test: [ - ExampleItem(desc: '指定开始时间', builder: _customStartTime), - ExampleItem(desc: '限制时分秒时间', builder: _customLimitTime), - ExampleItem(desc: '自定义时间选项', builder: _customItems), - ExampleItem(desc: '自定义选中选项', builder: _customSelectWidget), - ExampleItem(desc: '只有时分限制时间', builder: _customItemsOnlyHour), - ], - ); - } - - @Demo(group: 'datetimePicker') - Widget buildYearMonthDay(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_1.isEmpty ? '请选择' : selected_1, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_1 = '${selected['year'].toString().padLeft(4, '0')}' - '-${selected['month'].toString().padLeft(2, '0')}' - '-${selected['day'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } - - @Demo(group: 'datetimePicker') - Widget buildYearMonth(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_2.isEmpty ? '请选择' : selected_2, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_2 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useDay: false, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } - - @Demo(group: 'datetimePicker') - Widget buildMonthDay(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_3.isEmpty ? '请选择' : selected_3, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_3 = '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useYear: false, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } - - @Demo(group: 'datetimePicker') - Widget buildHourMinuteSecond(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_4.isEmpty ? '请选择' : selected_4, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_4 = '${selected['hour'].toString().padLeft(2, '0')}:' - '${selected['minute'].toString().padLeft(2, '0')}:' - '${selected['second'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useYear: false, - useMonth: false, - useDay: false, - useHour: true, - useMinute: true, - useSecond: true, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31, 4, 12, 20], - initialDate: [2023, 12, 31], - ); - }, - ); - } - - @Demo(group: 'datetimePicker') - Widget buildAll(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_5.isEmpty ? '请选择' : selected_5, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_5 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')} ' - '${selected['hour'].toString().padLeft(2, '0')}:' - '${selected['minute'].toString().padLeft(2, '0')}:' - '${selected['second'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useHour: true, - useMinute: true, - useSecond: true, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } - - @Demo(group: 'datetimePicker') - Widget buildWeekDay(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_6.isEmpty ? '请选择' : selected_6, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_6 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')} ' - '${weekDayList[selected['weekDay']! - 1]}'; - }); - Navigator.of(context).pop(); - }, - useWeekDay: true, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } - - @Demo(group: 'datetimePicker') - Widget buildWithTitle(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_7.isEmpty ? '请选择' : selected_7, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_7 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } - - @Demo(group: 'datetimePicker') - Widget buildWithoutTitle(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_8.isEmpty ? '请选择' : selected_8, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - // 不传或传空字符串、null,则不显示标题 - // title: '', - onConfirm: (selected) { - setState(() { - selected_8 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - ); - }, - ); - } - - @Demo(group: 'datetimePicker') - Widget buildWithoutHeader(BuildContext context) { - return TDatePicker( - header: false, - model: DatePickerModel( - useYear: true, - useMonth: true, - useDay: true, - useHour: true, - useMinute: true, - useSecond: true, - useWeekDay: false, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - dateInitial: [2012, 1, 1], - ), - onChange: (selected) { - print('onChange ${selected}'); - }, - ); - } - - @Demo(group: 'datetimePicker') - Widget _customStartTime(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_5.isEmpty ? '请选择' : selected_5, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_5 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')} ' - '${selected['hour'].toString().padLeft(2, '0')}:' - '${selected['minute'].toString().padLeft(2, '0')}:' - '${selected['second'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useYear: true, - useMonth: true, - useDay: true, - useHour: true, - useMinute: true, - useSecond: true, - dateStart: [2012, 1, 15, 12, 28, 11], - dateEnd: [2012, 6, 15, 12, 48, 32], - initialDate: [2012, 1, 15, 13, 20], - ); - }, - ); - } - - @Demo(group: 'datetimePicker') - Widget _customLimitTime(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_4.isEmpty ? '请选择' : selected_4, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_4 = '${selected['hour'].toString().padLeft(2, '0')}:' - '${selected['minute'].toString().padLeft(2, '0')}:' - '${selected['second'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useYear: false, - useMonth: false, - useDay: false, - useHour: true, - useMinute: true, - useSecond: true, - dateStart: [2023, 12, 31], - dateEnd: [2023, 12, 31, 4, 12, 20], - initialDate: [2023, 12, 31, 3, 02, 03], - ); - }, - ); - } - - @Demo(group: 'datetimePicker') - Widget _customItems(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_9.isEmpty ? '请选择' : selected_9, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_9 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')} ' - '${selected['hour'].toString().padLeft(2, '0')}:' - '${selected['minute'].toString().padLeft(2, '0')}:' - '${selected['second'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useHour: true, - useMinute: true, - useSecond: true, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - filterItems: (key, nums) { - if (key == DateTypeKey.minute) { - return [0, 15, 30]; - } - return nums; - }, - itemBuilder: (context, content, colIndex, index, - itemDistanceCalculator, distance) { - return colIndex == 5 - ? TText( - content, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: itemDistanceCalculator.calculateFontWeight( - context, distance), - fontSize: index % 2 == 0 ? 20 : 10, - color: index % 2 == 1 - ? TTheme.of(context).textColorPrimary - : TTheme.of(context).successColor6, - ), - ) - : null; - }, - ); - }, - ); - } - - @Demo(group: 'datetimePicker') - Widget _customItemsOnlyHour(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_9.isEmpty ? '请选择' : selected_9, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '只有时分', - onConfirm: (selected) { - Navigator.of(context).pop(); - }, - useYear: false, - useMonth: false, - useDay: false, - useSecond: false, - useHour: true, - useMinute: true, - dateStart: [2025, 1, 1, 20, 0, 0], - dateEnd: [2025, 1, 1, 23, 59, 0], - initialDate: [2025, 1, 1, 22, 46, 0], - ); - }, - ); - } - - @Demo(group: 'datetimePicker') - Widget _customSelectWidget(BuildContext context) { - return TCell( - title: '选择时间', - note: selected_9, - arrow: true, - onClick: (click) { - TPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - setState(() { - selected_9 = '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')} ' - '${selected['hour'].toString().padLeft(2, '0')}:' - '${selected['minute'].toString().padLeft(2, '0')}:' - '${selected['second'].toString().padLeft(2, '0')}'; - }); - Navigator.of(context).pop(); - }, - useHour: true, - useMinute: true, - useSecond: true, - dateStart: [1999, 01, 01], - dateEnd: [2023, 12, 31], - initialDate: [2012, 1, 1], - customSelectWidget: Container( - height: 40, - decoration: const BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.all(Radius.circular(6)), - ), - ), - ); - }, - ); - } -} diff --git a/tdesign-component/example/lib/page/t_form_page.dart b/tdesign-component/example/lib/page/t_form_page.dart index a25f8a90f..e2329b135 100644 --- a/tdesign-component/example/lib/page/t_form_page.dart +++ b/tdesign-component/example/lib/page/t_form_page.dart @@ -28,6 +28,84 @@ class _TFormPageState extends State { /// form 排列方式是否为水平 bool _isFormHorizontal = true; + /// 展示日期选择器弹窗 + void _showDatePicker(BuildContext context, { + required Function(List selected) onConfirm, + List? initialDate, + }) { + // 生成年/月/日数据 + final year = initialDate?[0] ?? 2012; + final month = initialDate?[1] ?? 1; + final day = initialDate?[2] ?? 1; + + final yearItems = List.generate(52, (i) => TPickerOption(label: '${1999 + i}年', value: 1999 + i)); + final monthItems = List.generate(12, (i) => TPickerOption(label: '${i + 1}月', value: i + 1)); + final dayItems = List.generate(31, (i) => TPickerOption(label: '${i + 1}日', value: i + 1)); + + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) => Container( + decoration: BoxDecoration( + color: TTheme.of(context).bgColorContainer, + borderRadius: BorderRadius.vertical( + top: Radius.circular(TTheme.of(context).radiusExtraLarge), + ), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 标题栏 + Padding( + padding: EdgeInsets.all(TTheme.of(context).spacer16), + child: Row( + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: Text( + '取消', + style: TextStyle( + color: TTheme.of(context).textColorSecondary, + ), + ), + ), + Expanded( + child: Center( + child: Text( + '选择时间', + style: TextStyle( + fontWeight: FontWeight.w600, + color: TTheme.of(context).textColorPrimary, + ), + ), + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Text( + '确认', + style: TextStyle( + color: TTheme.of(context).brandNormalColor, + ), + ), + ), + ], + ), + ), + // 选择器 + TPicker( + items: [yearItems, monthItems, dayItems], + initialValue: [year, month, day], + onChange: (v) => onConfirm(v.values), + ), + ], + ), + ), + ), + ); + } + /// 设置按钮是否可点击状态 /// true 表示处于 active 状态 bool horizontalButton = false; @@ -397,18 +475,17 @@ class _TFormPageState extends State { if (_formDisableState) { return; } - TPicker.showDatePicker(context, title: '选择时间', - onConfirm: (selected) { - setState(() { - _selected_1 = - '${selected['year'].toString().padLeft(4, '0')}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}'; - _formItemNotifier['birth']?.upDataForm(_selected_1); - }); - Navigator.of(context).pop(); - }, - dateStart: [1999, 01, 01], - dateEnd: [2050, 12, 31], - initialDate: [2012, 1, 1]); + _showDatePicker( + context, + initialDate: [2012, 1, 1], + onConfirm: (selected) { + setState(() { + _selected_1 = + '${selected[0].toString().padLeft(4, '0')}-${selected[1].toString().padLeft(2, '0')}-${selected[2].toString().padLeft(2, '0')}'; + _formItemNotifier['birth']?.upDataForm(_selected_1); + }); + }, + ); }, ), TFormItem( @@ -736,18 +813,17 @@ class _TFormPageState extends State { if (_formDisableState) { return; } - TPicker.showDatePicker(context, title: '选择时间', - onConfirm: (selected) { - setState(() { - _selected_1 = - '${selected['year'].toString().padLeft(4, '0')}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}'; - _formItemNotifier['birth']?.upDataForm(_selected_1); - }); - Navigator.of(context).pop(); - }, - dateStart: [1999, 01, 01], - dateEnd: [2050, 12, 31], - initialDate: [2012, 1, 1]); + _showDatePicker( + context, + initialDate: [2012, 1, 1], + onConfirm: (selected) { + setState(() { + _selected_1 = + '${selected[0].toString().padLeft(4, '0')}-${selected[1].toString().padLeft(2, '0')}-${selected[2].toString().padLeft(2, '0')}'; + _formItemNotifier['birth']?.upDataForm(_selected_1); + }); + }, + ); }, ), TFormItem( diff --git a/tdesign-component/example/lib/page/t_picker_page.dart b/tdesign-component/example/lib/page/t_picker_page.dart index fca664601..997c40235 100644 --- a/tdesign-component/example/lib/page/t_picker_page.dart +++ b/tdesign-component/example/lib/page/t_picker_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:tdesign_flutter/tdesign_flutter.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; +import '../../annotation/demo.dart'; import '../../base/example_widget.dart'; -import '../annotation/demo.dart'; class TPickerPage extends StatefulWidget { const TPickerPage({Key? key}) : super(key: key); @@ -12,418 +12,427 @@ class TPickerPage extends StatefulWidget { } class _TPickerPageState extends State { - String selected_1 = ''; - List data_1 = ['广州市', '韶关市', '深圳市', '珠海区', '汕头市']; - String selected_2 = ''; - String selected_3 = ''; - List> data_2 = []; - String selected_4 = ''; - String selected_5 = ''; - Map data_3 = { - '广东省': { - '深圳市': ['南山区南山区南山区南山区南山区', '宝安区', '罗湖区', '福田区'], - '佛山市': [''], - '广州市广州市广州市广州市广州市广州市广州市广州市广州市广州市广州市': ['花都区'] - }, - '广东省2': { - '深圳市': ['南山区南山区南山区南山区南山区', '罗湖区', '福田区'], - '广州市广州市广州市广州市广州市广州市广州市广州市广州市广州市广州市': ['花都区'] - }, - '重庆市': { - '重庆市重庆市重庆市重庆市重庆市重庆市重庆市': ['九龙坡区', '江北区'], - }, - '浙江省浙江省浙江省浙江省浙江省浙江省浙江省浙江省': { - '杭州市': ['西湖区', '余杭区', '萧山区'], - '宁波市': ['江东区', '北仑区', '奉化市'] - }, - '香港': { - '香港': ['九龙城区', '黄大仙区', '离岛区', '湾仔区'] - }, - }; + // ========== 数据定义 ========== - Map dataTest = { - '广东省': { - '深圳市': ['南山区', '宝安区', '罗湖区', '福田区'], - '广州市': ['天河区', '越秀区', '白云区', '花都区'], - '佛山市': ['顺德区', '南海区', '禅城区'] - }, - '浙江省': { - '杭州市': ['西湖区', '余杭区', '萧山区'], - '宁波市': ['江东区', '北仑区', '奉化市'], - '温州市': ['鹿城区', '瑞安市', '乐清市'] - }, - '江苏省': { - '南京市': [ - '玄武区', - '秦淮区', - '建邺区', - '鼓楼区', - '浦口区', - '栖霞区', - '雨花台区', - '江宁区', - '六合区', - '溧水区', - '高淳区' + final cityData = [ + [ + TPickerOption(label: '广州市', value: 'GZ'), + TPickerOption(label: '韶关市', value: 'SG'), + TPickerOption(label: '深圳市', value: 'SZ'), + TPickerOption(label: '珠海市', value: 'ZH'), + TPickerOption(label: '汕头市', value: 'ST'), + TPickerOption(label: '佛山市', value: 'FS'), + TPickerOption(label: '东莞市', value: 'DG'), + TPickerOption(label: '惠州市', value: 'HZ'), + ], + ]; + + String selectedCity = ''; + + // ========== 时分秒数据 ========== + + final timeData = [ + [ + for (int i = 0; i < 24; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}时', value: i), + ], + [ + for (int i = 0; i < 60; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}分', value: i), + ], + [ + for (int i = 0; i < 60; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}秒', value: i), + ], + ]; + + String selectedTime = ''; + + // ========== 联动数据(省市区)========== + + final linkedData = { + TPickerOption(label: '广东省', value: 'GD'): { + TPickerOption(label: '深圳市', value: 'SZ'): [ + TPickerOption(label: '南山区', value: 'NS'), + TPickerOption(label: '福田区', value: 'FT'), + TPickerOption(label: '宝安区', value: 'BA'), + TPickerOption(label: '罗湖区', value: 'LH'), + TPickerOption(label: '龙岗区', value: 'LG'), + ], + TPickerOption(label: '广州市', value: 'GZ'): [ + TPickerOption(label: '天河区', value: 'TH'), + TPickerOption(label: '越秀区', value: 'YX'), + TPickerOption(label: '白云区', value: 'BY'), + TPickerOption(label: '花都区', value: 'HD'), + ], + TPickerOption(label: '佛山市', value: 'FS'): [ + TPickerOption(label: '顺德区', value: 'SD'), + TPickerOption(label: '南海区', value: 'NH'), + TPickerOption(label: '禅城区', value: 'CC'), ], - '无锡市': ['梁溪区', '锡山区', '惠山区', '滨湖区', '新吴区'], - '徐州市': ['鼓楼区', '云龙区', '贾汪区', '泉山区', '铜山区'], - '常州市': ['天宁区', '钟楼区', '新北区', '武进区', '金坛区'], - '苏州市': ['姑苏区', '虎丘区', '吴中区', '相城区', '吴江区'], - '南通市': ['崇川区', '港闸区', '通州区'], - '连云港市': ['连云区', '海州区', '赣榆区'], - '淮安市': ['淮安区', '淮阴区', '清江浦区', '洪泽区'], - '盐城市': ['亭湖区', '盐都区', '大丰区', '建湖县'], - '扬州市': ['广陵区', '邗江区', '江都区'], - '镇江市': ['京口区', '润州区', '丹徒区'], - '泰州市': ['海陵区', '高港区', '姜堰区'], - '宿迁市': ['宿城区', '宿豫区'] - }, - '山东省': { - '济南市': ['历下区', '市中区', '天桥区'], - '青岛市': ['市南区', '市北区', '黄岛区'], - '烟台市': ['芝罘区', '莱山区'] - }, - '河南省': { - '郑州市': ['金水区', '中原区', '惠济区'], - '洛阳市': ['老城区', '西工区'], - '开封市': ['龙亭区', '顺河区'] - }, - '河北省': { - '石家庄市': ['长安区', '桥西区', '裕华区'], - '唐山市': ['路南区', '路北区'], - '邯郸市': ['丛台区', '邯山区'] - }, - '四川省': { - '成都市': ['锦江区', '青羊区', '武侯区'], - '绵阳市': ['涪城区', '游仙区'], - '德阳市': ['旌阳区', '罗江区'] - }, - '湖南省': { - '长沙市': ['岳麓区', '开福区', '天心区'], - '株洲市': ['天元区', '芦淞区'], - '湘潭市': ['雨湖区', '岳塘区'] - }, - '湖北省': { - '武汉市': ['江汉区', '武昌区', '洪山区'], - '宜昌市': ['西陵区', '伍家岗区'], - '襄阳市': ['樊城区', '襄城区'] - }, - '安徽省': { - '合肥市': ['庐阳区', '蜀山区', '包河区'], - '芜湖市': ['镜湖区', '弋江区'], - '马鞍山市': ['花山区', '雨山区'] - }, - '江西省': { - '南昌市': ['东湖区', '西湖区', '青山湖区'], - '九江市': ['濂溪区', '浔阳区'], - '赣州市': ['章贡区', '南康区'] - }, - '福建省': { - '福州市': ['鼓楼区', '台江区', '仓山区'], - '厦门市': ['思明区', '湖里区'], - '泉州市': ['丰泽区', '鲤城区'] - }, - '云南省': { - '昆明市': ['五华区', '盘龙区'], - '大理市': ['大理古城', '下关镇'], - '丽江市': ['古城区', '玉龙县'] - }, - '贵州省': { - '贵阳市': ['云岩区', '南明区'], - '遵义市': ['红花岗区', '汇川区'], - '安顺市': ['西秀区', '平坝区'] - }, - '陕西省': { - '西安市': ['碑林区', '莲湖区', '雁塔区'], - '咸阳市': ['秦都区', '渭城区'], - '宝鸡市': ['渭滨区', '金台区'] - }, - '山西省': { - '太原市': ['小店区', '迎泽区'], - '大同市': ['平城区', '云冈区'], - '运城市': ['盐湖区', '永济市'] - }, - '辽宁省': { - '沈阳市': ['和平区', '沈河区', '皇姑区'], - '大连市': ['中山区', '西岗区'], - '鞍山市': ['铁东区', '铁西区'] - }, - '吉林省': { - '长春市': ['朝阳区', '南关区'], - '吉林市': ['船营区', '昌邑区'], - '延边州': ['延吉市', '敦化市'] - }, - '黑龙江省': { - '哈尔滨市': ['道里区', '南岗区', '香坊区'], - '齐齐哈尔市': ['龙沙区', '铁锋区'], - '大庆市': ['萨尔图区', '龙凤区'] - }, - '北京市': { - '北京市': ['朝阳区', '海淀区', '东城区', '西城区'] - }, - '上海市': { - '上海市': ['浦东新区', '黄浦区', '静安区'] - }, - '重庆市': { - '重庆市': ['渝中区', '江北区', '九龙坡区', '南岸区'] }, - '香港': { - '香港': ['九龙城区', '黄大仙区', '湾仔区', '离岛区'] + TPickerOption(label: '浙江省', value: 'ZJ'): { + TPickerOption(label: '杭州市', value: 'HZ'): [ + TPickerOption(label: '西湖区', value: 'XH'), + TPickerOption(label: '余杭区', value: 'YH'), + TPickerOption(label: '萧山区', value: 'XS'), + ], + TPickerOption(label: '宁波市', value: 'NB'): [ + TPickerOption(label: '江东区', value: 'JD'), + TPickerOption(label: '北仑区', value: 'BL'), + ], }, - '澳门': { - '澳门': ['花地玛堂区', '圣安多尼堂区', '大堂区', '风顺堂区'] - } }; - @override - void initState() { - var list = []; - for (var i = 2022; i >= 2000; i--) { - list.add('${i}年'); - } - data_2.add(list); - data_2.add(['春', '夏', '秋', '冬']); - super.initState(); - } + String selectedLinked = ''; + + // ========== 项级 disabled 数据(覆盖开头/中间/结尾禁用)========== + + final itemDisabledData = [ + // 第1列:结尾禁用 + [ + TPickerOption(label: '男', value: 'M'), + TPickerOption(label: '女', value: 'F'), + TPickerOption(label: '保密', value: 'N', disabled: true), + ], + // 第2列:开头 + 中间 + 结尾 各 1 个禁用(稀疏分布,留足操作空间) + [ + TPickerOption(label: 'A排1座', value: 'A1', disabled: true), // 开头禁用 + TPickerOption(label: 'A排2座', value: 'A2'), + TPickerOption(label: 'A排3座', value: 'A3'), + TPickerOption(label: 'A排4座', value: 'A4'), + TPickerOption(label: 'A排5座', value: 'A5'), + TPickerOption(label: 'A排6座', value: 'A6', disabled: true), + TPickerOption(label: 'A排7座', value: 'A7', disabled: true), // 中间偏后禁用 + TPickerOption(label: 'A排8座', value: 'A8', disabled: true), // 新增禁用 + TPickerOption(label: 'A排9座', value: 'A9'), + TPickerOption(label: 'A排10座', value: 'A10'), + TPickerOption(label: 'A排11座', value: 'A11'), + TPickerOption(label: 'A排12座', value: 'A12', disabled: true), // 结尾禁用 + ], + ]; + + String selectedItemDisabled = ''; + + // ========== 全局 disabled 开关 ========== + + bool globalDisabled = false; + + // ========== 性别+偏好数据(弹窗专用)========== + + final preferenceData = [ + [ + TPickerOption(label: '男', value: 'M'), + TPickerOption(label: '女', value: 'F'), + TPickerOption(label: '其他', value: 'O'), + ], + [ + TPickerOption(label: '科技', value: 'tech'), + TPickerOption(label: '运动', value: 'sport'), + TPickerOption(label: '音乐', value: 'music'), + TPickerOption(label: '阅读', value: 'book'), + TPickerOption(label: '旅行', value: 'travel'), + TPickerOption(label: '美食', value: 'food'), + ], + ]; + + String selectedPreference = ''; @override Widget build(BuildContext context) { return ExamplePage( title: tTitle(), - desc: '用于一组预设数据中的选择。', + desc: '纯滚轮选择器组件,支持多列独立和联动两种模式', exampleCodeGroup: 'picker', children: [ - ExampleModule( - title: '组件类型', - children: [ - ExampleItem(desc: '基础选择器--地区', builder: buildArea), - ExampleItem(desc: '基础选择器--时间', builder: buildTime), - ExampleItem(desc: '基础选择器--地区--联动', builder: buildMultiArea), - ], - ), - ExampleModule( - title: '组件样式', - children: [ - ExampleItem(desc: '带标题选择器', builder: buildAreaWithTitle), - ExampleItem(desc: '无标题选择器', builder: buildAreaWithoutTitle), - ExampleItem(desc: '不使用弹窗、不带顶部内容', builder: buildWithoutHeader), - ], - ) + ExampleModule(title: '基础用法', children: [ + ExampleItem(desc: '单列选择', builder: buildSingleColumn), + ExampleItem(desc: '时间选择(时分秒)', builder: buildTimeSelect), + ExampleItem(desc: '联动选择(省市区)', builder: buildLinked), + ]), + ExampleModule(title: '按需请求', children: [ + ExampleItem(desc: '模拟网络请求加载更多', builder: buildLazyLoad), + ]), + ExampleModule(title: '禁用状态', children: [ + ExampleItem(desc: '项级 disabled(部分选项不可选)', builder: buildItemDisabled), + ExampleItem(desc: '全局 disabled(整组不可操作)', builder: buildGlobalDisabled), + ]), + ExampleModule(title: '弹窗模式(TPopup)', children: [ + ExampleItem(desc: '弹窗-联动选择(省市区)', builder: buildPopupLinked), + ExampleItem(desc: '弹窗-多列选择(性别/偏好)', builder: buildPopupMultiColumn), + ]), ], - test: [ - ExampleItem( - desc: '自定义left/right text', builder: buildCustomLeftRightText), - ExampleItem(desc: '级联选择保持下一级选项', builder: buildKeepMultiArea), + ); + } + + // ========== 弹窗工具方法 ========== + + void _showPickerPopup( + BuildContext context, { + required String title, + required Widget picker, + VoidCallback? onConfirm, + }) { + Navigator.of(context).push( + TSlidePopupRoute( + slideTransitionFrom: SlideTransitionFrom.bottom, + builder: (ctx) => TPopupBottomConfirmPanel( + title: title, + leftClick: () => Navigator.maybePop(ctx), + rightClick: () { + onConfirm?.call(); + Navigator.maybePop(ctx); + }, + child: picker, + ), + ), + ); + } + + // ========== 嵌入式容器 ========== + + Widget _pickerCard(BuildContext context, {required Widget child}) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: TTheme.of(context).bgColorContainer, + borderRadius: BorderRadius.circular(TTheme.of(context).radiusDefault), + ), + child: child, + ); + } + + // ========== 基础用法 ========== + + @Demo(group: 'picker') + Widget buildSingleColumn(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('选中城市: ${selectedCity.isEmpty ? "未选择" : selectedCity}', + style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + SizedBox(height: 8), + _pickerCard( + context, + child: TPicker(items: cityData, + onChange: (v) => setState(() => selectedCity = v.labels.first)), + ), ], ); } @Demo(group: 'picker') - Widget buildArea(BuildContext context) { - const title = '选择地区'; - return TCell( - title: title, - note: selected_1.isEmpty ? '请选择' : selected_1, - arrow: true, - onClick: (click) { - TPicker.showMultiPicker( + Widget buildTimeSelect(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('选中时间: ${selectedTime.isEmpty ? "未选择" : selectedTime}', + style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + SizedBox(height: 8), + _pickerCard( context, - title: title, - onConfirm: (selected) { - setState(() { - selected_1 = '${data_1[selected[0]]}'; - }); - Navigator.of(context).pop(); - }, - data: [data_1], - ); - }, + child: TPicker(items: timeData, itemCount: 5, + onChange: (v) => setState(() => + selectedTime = '${v.values[0]}:${v.values[1].toString().padLeft(2, '0')}:${v.values[2].toString().padLeft(2, '0')}')), + ), + ], ); } @Demo(group: 'picker') - Widget buildTime(BuildContext context) { - const title = '选择时间'; - return TCell( - title: title, - note: selected_2.isEmpty ? '请选择' : selected_2, - arrow: true, - onClick: (click) { - TPicker.showMultiPicker( + Widget buildLinked(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('选中地区: ${selectedLinked.isEmpty ? "未选择" : selectedLinked}', + style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + SizedBox(height: 8), + _pickerCard( context, - title: title, - onConfirm: (selected) { - print('selected ${selected}'); - setState(() { - selected_2 = - '${data_2[0][selected[0]]} ${data_2[1][selected[1]]}'; - }); - Navigator.of(context).pop(); - }, - data: data_2, - ); - }, + child: TPicker(items: linkedData, initialValue: ['GD'], + onChange: (v) => setState(() => selectedLinked = v.labels.join(' / '))), + ), + ], ); } + // ========== 禁用状态 ========== + @Demo(group: 'picker') - Widget buildMultiArea(BuildContext context) { - const title = '选择地区'; - return TCell( - title: title, - note: selected_3.isEmpty ? '请选择' : selected_3, - arrow: true, - onClick: (click) { - TPicker.showMultiLinkedPicker( + Widget buildItemDisabled(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('选中: ${selectedItemDisabled.isEmpty ? "未选择" : selectedItemDisabled}', + style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + SizedBox(height: 4), + Text('提示: 标灰的选项不可选(第1列「保密」、第2列「A排1座/A排6座/A排7座/A排8座/A排12座」)', + style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), + SizedBox(height: 8), + _pickerCard( context, - title: title, - onConfirm: (selected) { - setState(() { - selected_3 = '${selected[0]} ${selected[1]} ${selected[2]}'; - }); - Navigator.of(context).pop(); - }, - data: dataTest, - columnNum: 3, - initialData: ['浙江省', '杭州市', '西湖区'], - ); - }, + child: TPicker(items: itemDisabledData, initialValue: ['M', 'A5'], + onChange: (v) => setState(() => + selectedItemDisabled = '${v.labels.first} ${v.labels.last}')), + ), + ], ); } @Demo(group: 'picker') - Widget buildAreaWithTitle(BuildContext context) { - const title = '选择地区'; - return TCell( - title: title, - note: selected_4.isEmpty ? '请选择' : selected_4, - arrow: true, - onClick: (click) { - TPicker.showMultiPicker( + Widget buildGlobalDisabled(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Switch( + value: globalDisabled, + onChanged: (v) => setState(() => globalDisabled = v), + ), + SizedBox(width: 8), + Text(globalDisabled ? '已禁用' : '已启用', + style: TextStyle( + fontSize: 14, + color: globalDisabled + ? TTheme.of(context).errorNormalColor + : TTheme.of(context).successNormalColor)), + ], + ), + SizedBox(height: 8), + _pickerCard( context, - title: '带标题选择器', - onConfirm: (selected) { - setState(() { - selected_4 = '${data_1[selected[0]]}'; - }); - Navigator.of(context).pop(); - }, - data: [data_1], - ); - }, + child: TPicker(items: cityData, initialValue: ['GZ'], + onChange: (v) => debugPrint('选中: $v'), + disabled: globalDisabled), + ), + SizedBox(height: 4), + Text('切换开关可控制整个选择器的禁用/启用状态', + style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), + ], ); } + // ========== 弹窗模式 ========== + + /// 弹窗联动模式的临时选中值(仅点击确认后才写入 selectedLinked) + String _popupLinkedTemp = ''; + @Demo(group: 'picker') - Widget buildAreaWithoutTitle(BuildContext context) { + Widget buildPopupLinked(BuildContext context) { return TCell( - title: '选择地区', - note: selected_5.isEmpty ? '请选择' : selected_5, + title: '弹窗-联动选择(省市区)', + note: selectedLinked.isEmpty ? '请选择' : selectedLinked, arrow: true, - onClick: (click) { - TPicker.showMultiPicker( + onClick: (_) { + _showPickerPopup( context, - // 不传或传空字符串、null,则不显示标题 - // title: '', - onConfirm: (selected) { - setState(() { - selected_5 = '${data_1[selected[0]]}'; - }); - Navigator.of(context).pop(); - }, - data: [data_1], + title: '请选择地区', + picker: TPicker( + items: linkedData, + initialValue: selectedLinked.isNotEmpty + ? selectedLinked.split(' / ') + : ['GD'], + onChange: (v) => setState(() => _popupLinkedTemp = v.labels.join(' / ')), + ), + onConfirm: () => setState(() => selectedLinked = _popupLinkedTemp), ); }, ); } - @Demo(group: 'picker') - Widget buildWithoutHeader(BuildContext context) { - return TMultiPicker( - /// 不显示header内容 - header: false, - /// todo onChange - onConfirm: (selected) { - setState(() { - selected_5 = '${data_1[selected[0]]}'; - }); - Navigator.of(context).pop(); - }, - data: [data_1], - ); - } + /// 弹窗多列独立模式的临时选中值 + String _popupMultiColTemp = ''; + + /// 按需请求:模拟网络延迟加载更多数据 + List _lazyData = [ + for (int i = 1; i <= 20; i++) + TPickerOption(label: '选项 $i', value: 'opt_$i'), + ]; + bool _isLoading = false; @Demo(group: 'picker') - Widget buildCustomLeftRightText(BuildContext context) { - return TCellGroup( - cells: [ - TCell( - title: '基础选择器', - note: selected_5.isEmpty ? '请选择' : selected_5, - arrow: true, - onClick: (click) { - TPicker.showMultiPicker( - context, - leftText: '自定义取消', - rightText: '自定义确认', - title: '基础选择器', - onConfirm: (selected) { - setState(() { - selected_5 = '${data_1[selected[0]]}'; - }); - Navigator.of(context).pop(); - }, - data: [data_1], - ); - }, - ), - TCell( - title: '联动选择器', - note: selected_3.isEmpty ? '请选择' : selected_3, - arrow: true, - onClick: (click) { - TPicker.showMultiLinkedPicker( + Widget buildLazyLoad(BuildContext context) { + return StatefulBuilder( + builder: (ctx, setInner) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _lazyData.isEmpty + ? '暂无数据' + : '已加载 ${_lazyData.length} 条(滚动到底部自动加载更多)', + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary), + ), + const SizedBox(height: 8), + _pickerCard( context, - leftText: '自定义取消', - rightText: '自定义确认', - title: '联动选择器', - onConfirm: (selected) { - setState(() { - selected_3 = '${selected[0]} ${selected[1]} ${selected[2]}'; - }); - Navigator.of(context).pop(); - }, - data: data_3, - columnNum: 3, - initialData: ['浙江省', '杭州市', '西湖区'], - ); - }, - ) - ], + child: TPicker( + items: [_lazyData], + preloadThreshold: 5, + onLoad: (e) async { + if (_isLoading) return; + setInner(() => _isLoading = true); + // 模拟网络请求延迟 1.5s + await Future.delayed(const Duration(milliseconds: 1500)); + final start = _lazyData.length + 1; + final more = [ + for (int i = start; i < start + 20; i++) + TPickerOption(label: '选项 $i', value: 'opt_$i'), + ]; + setInner(() { + _lazyData.addAll(more); + _isLoading = false; + }); + }, + onChange: (v) => setState(() => + selectedPreference = v.labels.first), + ), + ), + const SizedBox(height: 4), + if (_isLoading) + Row( + children: [ + SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2), + ), + const SizedBox(width: 6), + Text('正在加载更多数据...', + style: TextStyle( + fontSize: 12, + color: TTheme.of(context).textColorPlaceholder)), + ], + ) + else + Text('距底部 5 项时触发 onLoad,模拟 1.5s 网络延迟', + style: TextStyle( + fontSize: 12, + color: TTheme.of(context).textColorPlaceholder)), + ], + ); + }, ); } @Demo(group: 'picker') - Widget buildKeepMultiArea(BuildContext context) { + Widget buildPopupMultiColumn(BuildContext context) { return TCell( - title: '选择地区', - note: selected_3.isEmpty ? '请选择' : selected_3, + title: '弹窗-多列选择(性别/偏好)', + note: selectedPreference.isEmpty ? '请选择' : selectedPreference, arrow: true, - onClick: (click) { - TPicker.showMultiLinkedPicker( + onClick: (_) { + _showPickerPopup( context, - title: '选择地区', - onConfirm: (selected) { - setState(() { - selected_3 = '${selected[0]} ${selected[1]} ${selected[2]}'; - }); - Navigator.of(context).pop(); - }, - data: data_3, - columnNum: 3, - keepSameSelection: true, - initialData: ['广东省', '深圳市', '罗湖区'], + title: '选择性别和偏好', + picker: TPicker( + items: preferenceData, + initialValue: selectedPreference.isNotEmpty + ? selectedPreference.split(' ') + : ['M', 'tech'], + onChange: (v) => setState(() => + _popupMultiColTemp = '${v.labels.first} ${v.labels.last}'), + ), + onConfirm: () => setState(() => selectedPreference = _popupMultiColTemp), ); }, ); diff --git a/tdesign-component/lib/src/components/calendar/date_picker_model.dart b/tdesign-component/lib/src/components/calendar/date_picker_model.dart new file mode 100644 index 000000000..abee1a70a --- /dev/null +++ b/tdesign-component/lib/src/components/calendar/date_picker_model.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; + +import '../../../tdesign_flutter.dart'; +import '../picker/no_wave_behavior.dart'; +import '../picker/t_item_widget.dart'; +import '../picker/t_picker_option.dart'; +import '../picker/t_picker_value.dart'; + +/// 日期选择器数据模型(供 TCalendar 内部时间选择器使用) +/// +/// 精简版,仅包含 TCalendar 时间选择器所需功能 +class DatePickerModel { + final bool useYear; + final bool useMonth; + final bool useDay; + final bool useHour; + final bool useMinute; + final bool useSecond; + final bool useWeekDay; + + /// 可选起始日期 [year, month, day, ...] + final List? dateStart; + + /// 可选结束日期 + final List? dateEnd; + + /// 默认选中的日期 [year, month, day, hour, minute, second, ...] + final List? dateInitial; + + /// 过滤选项 + final List Function(String key, List items)? filterItems; + + DatePickerModel({ + this.useYear = true, + this.useMonth = true, + this.useDay = true, + this.useHour = false, + this.useMinute = false, + this.useSecond = false, + this.useWeekDay = false, + this.dateStart, + this.dateEnd, + this.dateInitial, + this.filterItems, + }); + + /// 获取年数据列表 + List get years { + final start = (dateStart != null && dateStart!.isNotEmpty) ? dateStart![0] : 1900; + final end = (dateEnd != null && dateEnd!.isNotEmpty) ? dateEnd![0] : 2100; + return List.generate(end - start + 1, (i) => start + i); + } + + /// 获取月数据列表 + List get months => List.generate(12, (i) => i + 1); + + /// 获取日数据列表 + List days(int year, int month) { + final daysInMonth = DateTime(year, month + 1).subtract(const Duration(days: 1)).day; + return List.generate(daysInMonth, (i) => i + 1); + } + + /// 获取时数据列表 + List get hours => List.generate(24, (i) => i); + + /// 获取分数据列表 + List get minutes => List.generate(60, (i) => i); + + /// 获取秒数据列表 + List get seconds => List.generate(60, (i) => i); + + /// 获取星期数据列表 + List get weekDays => ['一', '二', '三', '四', '五', '六', '日']; + + /// 所有列的 ScrollController + late List controllers; + late List data; + + /// 命名控制器便捷访问(供 TCalendar 使用) + FixedExtentScrollController get hourFixedExtentScrollController { + int idx = 0; + if (useYear) idx++; + if (useMonth) idx++; + if (useDay) idx++; + return controllers[idx]; + } + + FixedExtentScrollController get minuteFixedExtentScrollController { + int idx = 0; + if (useYear) idx++; + if (useMonth) idx++; + if (useDay) idx++; + if (useHour) idx++; + return controllers[idx]; + } + + FixedExtentScrollController get secondFixedExtentScrollController { + int idx = 0; + if (useYear) idx++; + if (useMonth) idx++; + if (useDay) idx++; + if (useHour) idx++; + if (useMinute) idx++; + return controllers[idx]; + } + + /// 初始化 + void init() { + data = []; + controllers = []; + + if (useYear) data.add(years); + if (useMonth) data.add(months); + if (useDay) data.add([31]); // 占位,下面会刷新 + if (useHour) data.add(hours); + if (useMinute) data.add(minutes); + if (useSecond) data.add(seconds); + if (useWeekDay) data.add(weekDays); + + // 刷新日列数据 + if (useDay) _refreshDays(); + + controllers = List.generate( + data.length, + (_) => FixedExtentScrollController(), + ); + + // 设置初始位置 + if (dateInitial != null) { + final init = dateInitial!; + for (var i = 0; i < init.length && i < controllers.length; i++) { + if (i < data[i].length) { + final idx = data[i].indexOf(init[i]); + if (idx >= 0) controllers[i].jumpToItem(idx); + } + } + } + } + + /// 根据当前选中值刷新日列数据 + void _refreshDays() { + final yearIdx = useYear ? controllers[0].selectedItem : 0; + final monthIdx = useMonth ? controllers[1].selectedItem : 0; + final year = useYear ? years[yearIdx] : DateTime.now().year; + final month = useMonth ? months[monthIdx] : DateTime.now().month; + data[2] = days(year, month); + } + + /// 外部调用:当年/月变化时刷新后续列 + void refreshDataAndController(int changedColumn) { + if (changedColumn == 0 && useMonth) { + // 年变化 → 刷新月 + _refreshDays(); + if (controllers.length > changedColumn + 1) controllers[changedColumn + 1].jumpToItem(0); + } + if (changedColumn == 1 && useDay) { + // 月变化 → 刷新日 + _refreshDays(); + if (controllers.length > changedColumn + 1) controllers[changedColumn + 1].jumpToItem(0); + } + } + + /// 获取当前选中值 + Map get selected { + final result = {}; + var idx = 0; + if (useYear && idx < data.length) result['year'] = data[idx++][controllers[idx++].selectedItem]; + if (useMonth && idx < data.length) result['month'] = data[idx++][controllers[idx++].selectedItem]; + if (useDay && idx < data.length) result['day'] = data[idx++][controllers[idx++].selectedItem]; + if (useHour && idx < data.length) result['hour'] = data[idx++][controllers[idx++].selectedItem]; + if (useMinute && idx < data.length) result['minute'] = data[idx++][controllers[idx++].selectedItem]; + if (useSecond && idx < data.length) result['second'] = data[idx++][controllers[idx++].selectedItem]; + return result; + } +} diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart index 2ffc69d50..89822da4c 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import '../../../tdesign_flutter.dart'; import '../../util/context_extension.dart'; import '../../util/iterable_ext.dart'; +import 'date_picker_model.dart'; +import 't_date_picker.dart'; export 't_calendar_body.dart'; export 't_calendar_cell.dart'; diff --git a/tdesign-component/lib/src/components/calendar/t_date_picker.dart b/tdesign-component/lib/src/components/calendar/t_date_picker.dart new file mode 100644 index 000000000..9f7aef2ae --- /dev/null +++ b/tdesign-component/lib/src/components/calendar/t_date_picker.dart @@ -0,0 +1,154 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../../../tdesign_flutter.dart'; +import '../../util/context_extension.dart'; +import '../picker/no_wave_behavior.dart'; +import '../picker/t_item_widget.dart'; +import 'date_picker_model.dart'; +import '../picker/t_picker_option.dart'; +import '../picker/t_picker_value.dart'; + +/// 日期/时间选择器(供 TCalendar 内部使用) +/// +/// 精简版,仅提供 TCalendar 时间选择器所需功能 +class TDatePicker extends StatefulWidget { + final String? title; + final String? leftText; + final String? rightText; + final DatePickerModel model; + final double? pickerHeight; + final int? pickerItemCount; + final bool? isTimeUnit; + final void Function(Map)? onConfirm; + final void Function(int wheelIndex, int index)? onSelectedItemChanged; + + const TDatePicker({ + super.key, + this.title, + this.leftText, + this.rightText, + required this.model, + this.pickerHeight, + this.pickerItemCount, + this.isTimeUnit, + this.onConfirm, + this.onSelectedItemChanged, + }); + + @override + State createState() => _TDatePickerState(); +} + +class _TDatePickerState extends State { + late double _pickerHeight; + + @override + void initState() { + super.initState(); + _pickerHeight = widget.pickerHeight ?? 178; + widget.model.init(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.title != null) + Padding( + padding: EdgeInsets.symmetric(horizontal: TTheme.of(context).spacer16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: Text(widget.leftText ?? '取消', style: TextStyle(color: TTheme.of(context).textColorSecondary)), + ), + Text(widget.title ?? '', style: TextStyle(fontWeight: FontWeight.w600)), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Text(widget.rightText ?? '确认', style: TextStyle(color: TTheme.of(context).brandNormalColor)), + ), + ], + ), + ), + SizedBox( + height: _pickerHeight, + width: MediaQuery.of(context).size.width, + child: Stack( + alignment: Alignment.center, + children: [ + Positioned( + top: (_pickerHeight - 40) / 2, + left: 16, + right: 16, + child: Container( + height: 40, + decoration: BoxDecoration( + color: TTheme.of(context).bgColorSecondaryContainer, + borderRadius: BorderRadius.circular(TTheme.of(context).radiusDefault), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + children: List.generate( + widget.model.controllers.length, + (i) => Expanded(child: _buildColumn(i)), + ), + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildColumn(int colIndex) { + final data = widget.model.data[colIndex]; + if (data.isEmpty) return const SizedBox.shrink(); + + return MediaQuery.removePadding( + context: context, + removeTop: true, + child: ScrollConfiguration( + behavior: NoWaveBehavior(), + child: ListWheelScrollView.useDelegate( + itemExtent: _pickerHeight / (widget.pickerItemCount ?? 5), + diameterRatio: 100, + controller: widget.model.controllers[colIndex], + physics: const FixedExtentScrollPhysics(), + onSelectedItemChanged: (index) { + // 联动刷新 + if (colIndex < widget.model.data.length - 1) { + widget.model.refreshDataAndController(colIndex); + } + widget.onSelectedItemChanged?.call(colIndex, index); + }, + childDelegate: ListWheelChildBuilderDelegate( + childCount: data.length, + builder: (context, index) { + final content = data[index].toString(); + return Container( + alignment: Alignment.center, + height: _pickerHeight / (widget.pickerItemCount ?? 5), + width: MediaQuery.of(context).size.width, + child: TItemWidget( + content: content, + fixedExtentScrollController: widget.model.controllers[colIndex], + colIndex: colIndex, + index: index, + itemHeight: _pickerHeight / (widget.pickerItemCount ?? 5), + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/tdesign-component/lib/src/components/picker/t_date_picker.dart b/tdesign-component/lib/src/components/picker/t_date_picker.dart deleted file mode 100644 index 996778ecf..000000000 --- a/tdesign-component/lib/src/components/picker/t_date_picker.dart +++ /dev/null @@ -1,978 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -import '../../../tdesign_flutter.dart'; -import '../../util/context_extension.dart'; -import 'no_wave_behavior.dart'; - -typedef DatePickerCallback = void Function(Map selected); - -enum DateTypeKey { year, month, weekDay, day, hour, minute, second } - -/// 时间选择器 -class TDatePicker extends StatefulWidget { - const TDatePicker({ - this.title, - this.titleHeight, - this.titleDividerColor, - this.onConfirm, - this.onCancel, - this.onChange, - this.onSelectedItemChanged, - this.leftText, - this.rightText, - this.leftTextStyle, - this.centerTextStyle, - this.rightTextStyle, - this.padding, - this.leftPadding, - this.topPadding, - this.rightPadding, - this.topRadius, - this.backgroundColor, - this.customSelectWidget, - this.header = true, - this.itemDistanceCalculator, - required this.model, - this.pickerHeight = 200, - this.pickerItemCount = 5, - this.isTimeUnit, - this.itemBuilder, - Key? key, - }) : super(key: key); - - /// 选择器标题 - final String? title; - - /// 标题高度 - final double? titleHeight; - - /// 标题分割线颜色 - final Color? titleDividerColor; - - /// 选择器确认按钮回调 - final DatePickerCallback? onConfirm; - - /// 选择器取消按钮回调 - final DatePickerCallback? onCancel; - - /// 选择器值改变回调 - final DatePickerCallback? onChange; - - /// 选择器选中项改变回调 - final void Function(int wheelIndex, int index)? onSelectedItemChanged; - - /// 左侧按钮文案 - final String? leftText; - - /// 右侧按钮文案 - final String? rightText; - - /// 自定义左侧文案样式 - final TextStyle? leftTextStyle; - - /// 自定义中间文案样式 - final TextStyle? centerTextStyle; - - /// 自定义右侧文案样式 - final TextStyle? rightTextStyle; - - /// 适配padding - final EdgeInsets? padding; - - /// 顶部填充 - final double? topPadding; - - /// 左边填充 - final double? leftPadding; - - /// 右边填充 - final double? rightPadding; - - /// 顶部圆角 - final double? topRadius; - - /// 背景颜色 - final Color? backgroundColor; - - /// 根据距离计算字体颜色、透明度、粗细 - final ItemDistanceCalculator? itemDistanceCalculator; - - /// 是否显示头部内容 - final bool header; - - /// 选择器List的视窗高度,默认200 - final double pickerHeight; - - /// 选择器List视窗中item个数,pickerHeight / pickerItemCount,即item高度 - final int pickerItemCount; - - /// 自定义选择框样式 - final Widget? customSelectWidget; - - /// 数据模型 - final DatePickerModel model; - - /// 是否时间显示 - final bool? isTimeUnit; - - /// 自定义item构建 - final ItemBuilderType? itemBuilder; - - @override - State createState() => _TDatePickerState(); -} - -class _TDatePickerState extends State { - double pickerHeight = 0; - static const _pickerTitleHeight = 56.0; - - @override - void initState() { - super.initState(); - pickerHeight = widget.pickerHeight; - } - - @override - void dispose() { - widget.model.removeListener(); - super.dispose(); - } - - bool useAll() { - return widget.model.useYear && - widget.model.useMonth && - widget.model.useDay && - widget.model.useHour && - widget.model.useMinute && - widget.model.useSecond; - } - - selectListItem(String ev) { - var selected = { - 'year': widget.model.useYear - ? widget.model.yearFixedExtentScrollController.selectedItem + - widget.model.data[0][0] - : -1, - 'month': widget.model.useMonth - ? widget.model.monthFixedExtentScrollController.selectedItem + - widget.model.data[1][0] - : -1, - 'day': widget.model.useDay - ? widget.model.dayFixedExtentScrollController.selectedItem + - widget.model.data[2][0] - : -1, - 'weekDay': widget.model.useWeekDay - ? widget.model.weekDayFixedExtentScrollController.selectedItem + - widget.model.data[3][0] - : -1, - 'hour': widget.model.useHour - ? selectItemValue(widget.model.data[4], - widget.model.hourFixedExtentScrollController.selectedItem) - : -1, - 'minute': widget.model.useMinute - ? selectItemValue(widget.model.data[5], - widget.model.minuteFixedExtentScrollController.selectedItem) - : -1, - 'second': widget.model.useSecond - ? selectItemValue(widget.model.data[6], - widget.model.secondFixedExtentScrollController.selectedItem) - : -1, - }; - if (ev == 'onCancel' && widget.onCancel != null) { - widget.onCancel!(selected); - } else if (ev == 'onConfirm' && widget.onConfirm != null) { - widget.onConfirm!(selected); - } else if (ev == 'onChange' && widget.onChange != null) { - widget.onChange!(selected); - } else { - Navigator.of(context).pop(); - } - } - - int selectItemValue(List items, int itemIndex) { - /// 选择列表索引对应的项的值 - return items[itemIndex]; - } - - @override - Widget build(BuildContext context) { - var maxWidth = MediaQuery.of(context).size.width; - return Container( - width: maxWidth, - padding: widget.padding ?? - EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), - decoration: BoxDecoration( - color: widget.backgroundColor ?? TTheme.of(context).bgColorContainer, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - widget.topRadius ?? TTheme.of(context).radiusExtraLarge), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.header) _buildHeader(context), - SizedBox( - height: pickerHeight, - child: Stack( - alignment: Alignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16, right: 16), - child: widget.customSelectWidget ?? - Container( - height: 40, - decoration: BoxDecoration( - color: - TTheme.of(context).bgColorSecondaryContainer, - borderRadius: BorderRadius.all(Radius.circular( - TTheme.of(context).radiusDefault))), - ), - ), - Container( - height: pickerHeight, - width: maxWidth, - padding: const EdgeInsets.only(left: 32, right: 32), - child: Row( - children: [ - widget.model.useYear - ? useAll() - ? SizedBox( - child: buildList(context, 0), - width: 64, - ) - : Expanded(child: buildList(context, 0)) - : Container(), - widget.model.useMonth - ? Expanded(child: buildList(context, 1)) - : Container(), - widget.model.useDay - ? Expanded(child: buildList(context, 2)) - : Container(), - widget.model.useWeekDay - ? Expanded(child: buildList(context, 3)) - : Container(), - widget.model.useHour - ? Expanded(child: buildList(context, 4)) - : Container(), - widget.model.useMinute - ? Expanded(child: buildList(context, 5)) - : Container(), - widget.model.useSecond - ? Expanded(child: buildList(context, 6)) - : Container(), - ], - )), - // 蒙层 - Positioned( - top: 0, - child: IgnorePointer( - ignoring: true, - child: Container( - height: _pickerTitleHeight, - width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - TTheme.of(context).bgColorContainer, - TTheme.of(context).bgColorContainer.withOpacity(0) - ])), - ), - ), - ), - Positioned( - bottom: 0, - child: IgnorePointer( - ignoring: true, - child: Container( - height: _pickerTitleHeight, - width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - TTheme.of(context).bgColorContainer, - TTheme.of(context).bgColorContainer.withOpacity(0) - ])), - ), - ), - ) - ], - ), - ) - ], - ), - ); - } - - Widget buildList(context, int whichLine) { - /// whichLine参数表示这个列表表示的是年,还是月还是日...... - var maxWidth = MediaQuery.of(context).size.width; - return MediaQuery.removePadding( - context: context, - removeTop: true, - child: ScrollConfiguration( - behavior: NoWaveBehavior(), - child: ListWheelScrollView.useDelegate( - itemExtent: pickerHeight / widget.pickerItemCount, - diameterRatio: 100, - controller: widget.model.controllers[whichLine], - physics: whichLine == 3 - ? const NeverScrollableScrollPhysics() - : const FixedExtentScrollPhysics(), - onSelectedItemChanged: (index) { - if (whichLine == 0 || - whichLine == 1 || - whichLine == 2 || - whichLine == 4 || - whichLine == 5 || - whichLine == 6) { - // 年月的改变会引起日的改变, 年的改变会引起月的改变 - setState(() { - switch (whichLine) { - case 0: - widget.model.refreshMonthDataAndController(); - widget.model.refreshDayDataAndController(); - if (widget.model.useWeekDay) { - widget.model.refreshWeekDayDataAndController(); - } - break; - case 1: - widget.model.refreshDayDataAndController(); - if (widget.model.useWeekDay) { - widget.model.refreshWeekDayDataAndController(); - } - break; - case 2: - if (widget.model.useWeekDay) { - widget.model.refreshWeekDayDataAndController(); - } - break; - } - if (useAll()) { - widget.model - .refreshTimeDataInitialAndController(whichLine); - } - - /// 使用动态高度,强制列表组件的state刷新,以展现更新的数据,详见下方链接 - /// FIX:https://github.com/flutter/flutter/issues/22999 - pickerHeight = - pickerHeight - Random().nextDouble() / 100000000; - }); - } - if (widget.onChange != null) { - selectListItem('onChange'); - } - widget.onSelectedItemChanged?.call(whichLine, index); - }, - childDelegate: ListWheelChildBuilderDelegate( - childCount: widget.model.data[whichLine].length, - builder: (context, index) { - return Container( - alignment: Alignment.center, - height: pickerHeight / widget.pickerItemCount, - width: maxWidth, - child: TItemWidget( - colIndex: whichLine, - index: index, - itemHeight: pickerHeight / widget.pickerItemCount, - content: whichLine == 3 - ? weekUnitMap( - widget.model.data[whichLine][index] - 1) - : widget.model.data[whichLine][index].toString() + - timeUnitMap(widget.model.mapping[whichLine]), - fixedExtentScrollController: - widget.model.controllers[whichLine], - itemDistanceCalculator: widget.itemDistanceCalculator, - itemBuilder: widget.itemBuilder, - )); - })), - )); - } - - Widget _buildHeader(BuildContext context) { - final padding = TTheme.of(context).spacer16; - - return Container( - padding: EdgeInsets.only( - left: widget.leftPadding ?? padding, - right: widget.rightPadding ?? padding, - top: widget.topPadding ?? padding, - ), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 0.5, - color: widget.titleDividerColor ?? Colors.transparent, - ), - ), - ), - - /// 减去分割线的空间 - height: getTitleHeight() - 0.5, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - /// 左边按钮 - GestureDetector( - onTap: () { - selectListItem('onCancel'); - }, - behavior: HitTestBehavior.opaque, - child: TText( - widget.leftText ?? context.resource.cancel, - style: widget.leftTextStyle ?? - TextStyle( - fontSize: TTheme.of(context).fontBodyLarge!.size, - color: TTheme.of(context).textColorSecondary), - ), - ), - - /// 中间title - Expanded( - child: Center( - child: TText( - widget.title ?? '', - style: widget.centerTextStyle ?? - TextStyle( - fontSize: TTheme.of(context).fontBodyLarge!.size, - fontWeight: FontWeight.w600, - color: TTheme.of(context).textColorPrimary, - ), - ), - ), - ), - - /// 右边按钮 - GestureDetector( - onTap: () { - selectListItem('onConfirm'); - }, - behavior: HitTestBehavior.opaque, - child: TText( - widget.rightText ?? context.resource.confirm, - style: widget.rightTextStyle ?? - TextStyle( - fontSize: TTheme.of(context).fontBodyLarge!.size, - color: TTheme.of(context).brandNormalColor), - ), - ), - ], - ), - ); - } - - timeUnitMap(String name) { - if (widget.isTimeUnit != null && widget.isTimeUnit == true) { - // 使用 widget.model.mapping 做为键 - var times = { - widget.model.mapping[0]: context.resource.yearLabel, - widget.model.mapping[1]: context.resource.monthLabel, - widget.model.mapping[2]: context.resource.dateLabel, - widget.model.mapping[3]: context.resource.weeksLabel, - widget.model.mapping[4]: context.resource.hours, - widget.model.mapping[5]: context.resource.minutes, - widget.model.mapping[6]: context.resource.seconds - }; - return times[name] ?? ''; - } else { - return ''; - } - } - - weekUnitMap(int index) { - if (index < 0 || index > 6) { - return ''; - } - return [ - context.resource.monday, - context.resource.tuesday, - context.resource.wednesday, - context.resource.thursday, - context.resource.friday, - context.resource.saturday, - context.resource.sunday, - ][index]; - } - - double getTitleHeight() => widget.titleHeight ?? _pickerTitleHeight; -} - -// 时间选择器的数据类 -class DatePickerModel { - bool useYear; - bool useMonth; - bool useDay; - bool useWeekDay; - bool useHour; - bool useMinute; - bool useSecond; - List dateStart; - List dateEnd; - List? dateInitial; - List Function(DateTypeKey key, List nums)? filterItems; - final mapping = [ - 'year', - 'month', - 'date', - 'weeks', - 'hours', - 'minutes', - 'seconds' - ]; - - late DateTime initialTime; - - /// 这四项随滑动而更新,注意初始化 - late int yearIndex; - late int monthIndex; - late int dayIndex; - late int weekDayIndex; - late int hourIndex; - late int minuteIndex; - late int secondIndex; - late List> data = [ - [], - [], - [], - [], - [], - [], - [], - ]; - late var controllers; - late FixedExtentScrollController yearFixedExtentScrollController; - late FixedExtentScrollController monthFixedExtentScrollController; - late FixedExtentScrollController dayFixedExtentScrollController; - late FixedExtentScrollController weekDayFixedExtentScrollController; - late FixedExtentScrollController hourFixedExtentScrollController; - late FixedExtentScrollController minuteFixedExtentScrollController; - late FixedExtentScrollController secondFixedExtentScrollController; - - DatePickerModel({ - required this.useYear, - required this.useMonth, - required this.useDay, - required this.useHour, - required this.useMinute, - required this.useSecond, - required this.useWeekDay, - required this.dateStart, - required this.dateEnd, - this.dateInitial, - this.filterItems, - }) { - assert(!useWeekDay || (!useSecond && !useMinute && !useHour), - 'WeekDay can only used with Year, Month and Day!'); - setInitialTime(); - setInitialYearData(); - setInitialMonthData(); - setInitialDayData(); - setInitialTimeData(); - setInitialWeekDayData(); - setControllers(); - addListener(); - } - - void setInitialTime() { - dateStart = List.generate( - 6, (index) => index < dateStart.length ? dateStart[index] : 0); - var startTime = DateTime(dateStart[0], dateStart[1], dateStart[2], - dateStart[3], dateStart[4], dateStart[5]); - dateEnd = List.generate( - 6, (index) => index < dateEnd.length ? dateEnd[index] : 0); - var endTime = DateTime( - dateEnd[0], - dateEnd[1], - dateEnd[2], - dateEnd[3], - dateEnd[4], - dateEnd[5], - ); - if (dateInitial != null) { - var initList = List.generate( - 6, (index) => index < dateInitial!.length ? dateInitial![index] : 0); - initialTime = DateTime(initList[0], initList[1], initList[2], initList[3], - initList[4], initList[5]); - if (initialTime.isBefore(startTime)) { - initialTime = startTime; - } else if (initialTime.isAfter(endTime)) { - initialTime = endTime; - } - return; - } - - var now = DateTime.now(); - if (now.isBefore(startTime)) { - initialTime = startTime.copyWith(); - } else if (now.isAfter(endTime)) { - initialTime = startTime.copyWith(); - } else { - initialTime = now; - } - } - - void setInitialYearData() { - var years = List.generate( - dateEnd[0] - dateStart[0] + 1, (index) => index + dateStart[0]); - data[0] = useYear && filterItems != null - ? filterItems!(DateTypeKey.year, years) - : years; - } - - void setInitialMonthData() { - late List month; - if (dateEnd[0] == dateStart[0]) { - month = List.generate( - dateEnd[1] - dateStart[1] + 1, (index) => index + dateStart[1]); - } else if (initialTime.year == dateStart[0]) { - month = - List.generate(12 - dateStart[1] + 1, (index) => index + dateStart[1]); - } else if (initialTime.year == dateEnd[0]) { - month = List.generate(dateEnd[1], (index) => index + 1); - } else { - month = List.generate(12, (index) => index + 1); - } - data[1] = useMonth && filterItems != null - ? filterItems!(DateTypeKey.month, month) - : month; - } - - void setInitialDayData() { - late List day; - if (dateEnd[0] == dateStart[0] && dateEnd[1] == dateStart[1]) { - day = List.generate( - dateEnd[2] - dateStart[2] + 1, (index) => index + dateStart[2]); - } else if (initialTime.year == dateStart[0] && - initialTime.month == dateStart[1]) { - day = List.generate( - DateTime(initialTime.year, initialTime.month + 1, 0).day - - dateStart[2] + - 1, - (index) => index + dateStart[2]); - } else if (initialTime.year == dateEnd[0] && - initialTime.month == dateEnd[1]) { - day = List.generate(dateEnd[2], (index) => index + 1); - } else { - day = List.generate( - DateTime(initialTime.year, initialTime.month + 1, 0).day, - (index) => index + 1); - } - data[2] = useDay && filterItems != null - ? filterItems!(DateTypeKey.day, day) - : day; - } - - void setInitialWeekDayData() { - var weekDay = [1, 2, 3, 4, 5, 6, 7]; - data[3] = useWeekDay && filterItems != null - ? filterItems!(DateTypeKey.weekDay, weekDay) - : weekDay; - } - - void setInitialTimeData() { - var hour = List.generate(24, (index) => index); - var minute = List.generate(60, (index) => index); - var second = List.generate(60, (index) => index); - if (dateStart.length > 3) { - if (!useYear && - !useMonth && - !useDay && - dateEnd[0] == dateStart[0] && - dateEnd[1] == dateStart[1] && - dateEnd[2] == dateStart[2]) { - hour = List.generate( - max(0, dateEnd[3] - dateStart[3] + 1), (i) => i + dateStart[3]); - minute = List.generate( - max(0, dateEnd[4] - dateStart[4] + 1), (i) => i + dateStart[4]); - second = List.generate( - max(0, dateEnd[5] - dateStart[5] + 1), (i) => i + dateStart[5]); - - data[4] = useHour && filterItems != null - ? filterItems!(DateTypeKey.hour, hour) - : hour; - data[5] = useMinute && filterItems != null - ? filterItems!(DateTypeKey.minute, minute) - : minute; - data[6] = useSecond && filterItems != null - ? filterItems!(DateTypeKey.second, second) - : second; - return; - } - if (initialTime.hour >= dateStart[3]) { - hour = - List.generate(24 - dateStart[3], (index) => index + dateStart[3]); - } - if (initialTime.minute >= dateStart[4]) { - minute = - List.generate(60 - dateStart[4], (index) => index + dateStart[4]); - } - if (initialTime.second >= dateStart[5]) { - second = - List.generate(60 - dateStart[5], (index) => index + dateStart[5]); - } - } - data[4] = useHour && filterItems != null - ? filterItems!(DateTypeKey.hour, hour) - : hour; - data[5] = useMinute && filterItems != null - ? filterItems!(DateTypeKey.minute, minute) - : minute; - data[6] = useSecond && filterItems != null - ? filterItems!(DateTypeKey.second, second) - : second; - } - - void setControllers() { - /// 初始化Index - yearIndex = initialTime.year - data[0][0]; - monthIndex = initialTime.month - data[1][0]; - dayIndex = initialTime.day - data[2][0]; - weekDayIndex = initialTime.weekday - 1; - hourIndex = initialTime.hour - data[4][0]; - minuteIndex = initialTime.minute - data[5][0]; - secondIndex = initialTime.second - data[6][0]; - controllers = [ - FixedExtentScrollController(initialItem: yearIndex), - FixedExtentScrollController(initialItem: monthIndex), - FixedExtentScrollController(initialItem: dayIndex), - FixedExtentScrollController(initialItem: weekDayIndex), - FixedExtentScrollController(initialItem: hourIndex), - FixedExtentScrollController(initialItem: minuteIndex), - FixedExtentScrollController(initialItem: secondIndex) - ]; - yearFixedExtentScrollController = controllers[0]; - monthFixedExtentScrollController = controllers[1]; - dayFixedExtentScrollController = controllers[2]; - weekDayFixedExtentScrollController = controllers[3]; - hourFixedExtentScrollController = controllers[4]; - minuteFixedExtentScrollController = controllers[5]; - secondFixedExtentScrollController = controllers[6]; - } - - void _yearListener() { - yearIndex = yearFixedExtentScrollController.selectedItem; - } - - void _monthListener() { - monthIndex = monthFixedExtentScrollController.selectedItem; - } - - void _dayListener() { - dayIndex = dayFixedExtentScrollController.selectedItem; - } - - void _weekDayListener() { - weekDayIndex = weekDayFixedExtentScrollController.selectedItem; - } - - void _hourDayListener() { - hourIndex = hourFixedExtentScrollController.selectedItem; - } - - void _minuteDayListener() { - minuteIndex = minuteFixedExtentScrollController.selectedItem; - } - - void _secondDayListener() { - secondIndex = secondFixedExtentScrollController.selectedItem; - } - - void addListener() { - /// 给年月日加上监控 - yearFixedExtentScrollController.addListener(_yearListener); - monthFixedExtentScrollController.addListener(_monthListener); - dayFixedExtentScrollController.addListener(_dayListener); - weekDayFixedExtentScrollController.addListener(_weekDayListener); - hourFixedExtentScrollController.addListener(_hourDayListener); - minuteFixedExtentScrollController.addListener(_minuteDayListener); - secondFixedExtentScrollController.addListener(_secondDayListener); - } - - void removeListener() { - /// 移除年月日的监控 - yearFixedExtentScrollController.removeListener(_yearListener); - monthFixedExtentScrollController.removeListener(_monthListener); - dayFixedExtentScrollController.removeListener(_dayListener); - weekDayFixedExtentScrollController.removeListener(_weekDayListener); - hourFixedExtentScrollController.removeListener(_hourDayListener); - minuteFixedExtentScrollController.removeListener(_minuteDayListener); - secondFixedExtentScrollController.removeListener(_secondDayListener); - } - - void refreshMonthDataAndController() { - var selectedYear = yearIndex + data[0][0]; - late List month; - if (dateEnd[0] == dateStart[0]) { - month = List.generate( - dateEnd[1] - dateStart[1] + 1, (index) => index + dateStart[1]); - } else if (selectedYear == dateStart[0]) { - month = - List.generate(12 - dateStart[1] + 1, (index) => index + dateStart[1]); - } else if (selectedYear == dateEnd[0]) { - month = List.generate(dateEnd[1], (index) => index + 1); - } else { - month = List.generate(12, (index) => index + 1); - } - data[1] = useMonth && filterItems != null - ? filterItems!(DateTypeKey.month, month) - : month; - monthFixedExtentScrollController.jumpToItem( - monthIndex > data[1].length - 1 ? data[1].length - 1 : monthIndex); - } - - void refreshDayDataAndController() { - /// 在刷新日数据时,年月数据已经是最新的 - var selectedYear = yearIndex + data[0][0]; - var selectedMonth = monthIndex + data[1][0]; - late List day; - if (dateEnd[0] == dateStart[0] && dateEnd[1] == dateStart[1]) { - day = List.generate( - dateEnd[2] - dateStart[2] + 1, (index) => index + dateStart[2]); - } else if (selectedYear == dateStart[0] && selectedMonth == dateStart[1]) { - day = List.generate( - DateTime(selectedYear, selectedMonth + 1, 0).day - dateStart[2] + 1, - (index) => index + dateStart[2]); - } else if (selectedYear == dateEnd[0] && selectedMonth == dateEnd[1]) { - day = List.generate(dateEnd[2], (index) => index + 1); - } else { - day = List.generate(DateTime(selectedYear, selectedMonth + 1, 0).day, - (index) => index + 1); - } - data[2] = useDay && filterItems != null - ? filterItems!(DateTypeKey.day, day) - : day; - dayFixedExtentScrollController.jumpToItem( - dayIndex > data[2].length - 1 ? data[2].length - 1 : dayIndex); - } - - void refreshWeekDayDataAndController() { - var date = DateTime( - data[0][yearFixedExtentScrollController.selectedItem], - data[1][monthFixedExtentScrollController.selectedItem], - data[2][dayFixedExtentScrollController.selectedItem]); - weekDayFixedExtentScrollController.jumpToItem(date.weekday - 1); - } - - void refreshTimeDataInitialAndController(int wheel) { - var selectedYear = yearIndex + data[0][0]; - var selectedMonth = monthIndex + data[1][0]; - var selectDay = dayIndex + data[2][0]; - if (wheel <= 2) { - refreshHourData( - selectedYear: selectedYear, - selectedMonth: selectedMonth, - selectDay: selectDay); - refreshMinuteData( - selectedYear: selectedYear, - selectedMonth: selectedMonth, - selectDay: selectDay); - } else { - refreshMinuteData( - selectedYear: selectedYear, - selectedMonth: selectedMonth, - selectDay: selectDay); - } - refreshSecondData( - selectedYear: selectedYear, - selectedMonth: selectedMonth, - selectDay: selectDay); - } - - void refreshHourData( - {required int selectedYear, - required int selectedMonth, - required int selectDay}) { - var selectHour = selectDay == data[2][0] ? 0 : hourIndex; - late List hour; - if (selectedYear == dateStart[0] && - selectedMonth == dateStart[1] && - selectDay == dateStart[2]) { - hour = - List.generate(24 - (dateStart[3]), (index) => index + dateStart[3]); - } else if (selectedYear == dateEnd[0] && - selectedMonth == dateEnd[1] && - selectDay == dateEnd[2]) { - hour = dateEnd[3] >= dateStart[3] - ? List.generate(dateEnd[3] + 1, (index) => index) - : List.generate(24 - dateStart[3], (index) => index); - } else { - hour = List.generate(24, (index) => index); - } - data[4] = useHour && filterItems != null - ? filterItems!(DateTypeKey.hour, hour) - : hour; - hourFixedExtentScrollController.jumpToItem(selectHour > 0 ? hourIndex : 0); - } - - void refreshMinuteData( - {required int selectedYear, - required int selectedMonth, - required int selectDay}) { - var selectHour = hourIndex + data[4][0]; - late List minute; - if (selectedYear == dateStart[0] && - selectedMonth == dateStart[1] && - selectDay == dateStart[2] && - selectHour == dateStart[3]) { - minute = - List.generate(60 - (dateStart[4]), (index) => index + dateStart[4]); - } else if (selectedYear == dateEnd[0] && - selectedMonth == dateEnd[1] && - selectDay == dateEnd[2] && - selectHour == dateEnd[3]) { - minute = dateEnd[4] >= dateStart[4] - ? List.generate(dateEnd[4] + 1, (index) => index) - : List.generate(60 - (dateStart[4]), (index) => index); - } else { - minute = List.generate(60, (index) => index); - } - data[5] = useMinute && filterItems != null - ? filterItems!(DateTypeKey.minute, minute) - : minute; - } - - void refreshSecondData( - {required int selectedYear, - required int selectedMonth, - required int selectDay}) { - var selectHour = hourIndex + data[4][0]; - var selectMinute = minuteIndex + data[5][0]; - late List second; - if (selectedYear == dateStart[0] && - selectedMonth == dateStart[1] && - selectDay == dateStart[2] && - selectHour == dateStart[3] && - selectMinute == dateStart[4]) { - second = - List.generate(60 - (dateStart[5]), (index) => index + dateStart[5]); - } else if (selectedYear == dateEnd[0] && - selectedMonth == dateEnd[1] && - selectDay == dateEnd[2] && - selectHour == dateEnd[3] && - selectMinute == dateEnd[4]) { - second = dateEnd[5] >= dateStart[5] - ? List.generate(dateEnd[5] + 1, (index) => index) - : List.generate(60 - (dateStart[5]), (index) => index); - } else { - second = List.generate(60, (index) => index); - } - data[6] = useSecond && filterItems != null - ? filterItems!(DateTypeKey.second, second) - : second; - } - - Map getSelectedMap() { - var map = { - 'year': yearIndex + data[0][0], - 'month': monthIndex + data[1][0], - 'day': dayIndex + data[2][0], - }; - return map; - } -} diff --git a/tdesign-component/lib/src/components/picker/t_item_widget.dart b/tdesign-component/lib/src/components/picker/t_item_widget.dart index 5d04eec79..4ad0afdee 100644 --- a/tdesign-component/lib/src/components/picker/t_item_widget.dart +++ b/tdesign-component/lib/src/components/picker/t_item_widget.dart @@ -23,25 +23,27 @@ typedef ItemBuilderType = Widget? Function( /// 所有选择器的子项组件 class TItemWidget extends StatefulWidget { - final String content; - final FixedExtentScrollController fixedExtentScrollController; - final int colIndex; - final int index; - final double itemHeight; - final ItemDistanceCalculator? itemDistanceCalculator; - final ItemBuilderType? itemBuilder; - const TItemWidget({ required this.fixedExtentScrollController, required this.colIndex, required this.index, required this.content, required this.itemHeight, + this.disabled = false, this.itemDistanceCalculator, this.itemBuilder, Key? key, }) : super(key: key); + final String content; + final FixedExtentScrollController fixedExtentScrollController; + final int colIndex; + final int index; + final double itemHeight; + final bool disabled; + final ItemDistanceCalculator? itemDistanceCalculator; + final ItemBuilderType? itemBuilder; + @override _TItemWidgetState createState() => _TItemWidgetState(); } @@ -64,33 +66,56 @@ class _TItemWidgetState extends State { @override Widget build(BuildContext context) { /// 子项此时离中心的距离 - /// 不要使用widget.fixedExtentScrollController.selectedItem - /// 其中selectedItem会报错,原因是一开始minScrollExtent为空 var distance = (widget.fixedExtentScrollController.offset / widget.itemHeight - widget.index) .abs() .toDouble(); _itemDistanceCalculator ??= ItemDistanceCalculator(); - return widget.itemBuilder?.call( - context, - widget.content, - widget.colIndex, - widget.index, - _itemDistanceCalculator!, - distance, - ) ?? - TText( - widget.content, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: - _itemDistanceCalculator!.calculateFontWeight(context, distance), - fontSize: _itemDistanceCalculator!.calculateFont(context, distance), - color: _itemDistanceCalculator!.calculateColor(context, distance), + + // disabled 项:使用默认禁用样式(opacity=0.5, 灰色, w400) + if (widget.disabled) { + return Center( + child: Opacity( + opacity: 0.5, + child: TText( + widget.content, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: _itemDistanceCalculator!.calculateFont(context, 0), + color: TTheme.of(context).textDisabledColor, + ), ), - ); + ), + ); + } + + return Center( + child: Opacity( + opacity: _itemDistanceCalculator!.calculateOpacity(distance), + child: widget.itemBuilder?.call( + context, + widget.content, + widget.colIndex, + widget.index, + _itemDistanceCalculator!, + distance, + ) ?? + TText( + widget.content, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: _itemDistanceCalculator! + .calculateFontWeight(context, distance), + fontSize: _itemDistanceCalculator!.calculateFont(context, distance), + color: _itemDistanceCalculator!.calculateColor(context, distance), + ), + ), + ), + ); } @override @@ -104,24 +129,47 @@ class _TItemWidgetState extends State { class ItemDistanceCalculator { ItemDistanceCalculator(); + /// 距离 → 整数档位(0=选中, 1=紧邻, 2=近边, 3+=远边) + static int _level(double distance) => distance.round().clamp(0, 3); + + /// 颜色:按档位离散赋值(不用 lerp 渐变) Color calculateColor(BuildContext context, double distance) { - /// 线性插值 - if (distance < 0.5) { - return TTheme.of(context).textColorPrimary; - } else { - return TTheme.of(context).textDisabledColor; + final primary = TTheme.of(context).textColorPrimary; + final placeholder = TTheme.of(context).textColorPlaceholder; + switch (_level(distance)) { + case 0: return primary; // 选中:纯主色 + case 1: return Color.lerp(primary, placeholder, 0.55) ?? primary; // 紧邻:55%占位色 + case 2: return Color.lerp(primary, placeholder, 0.78) ?? placeholder; // 近边:78%占位色 + default: return placeholder; // 远边/边缘:纯占位色 } } + /// 粗细:按档位离散赋值 FontWeight calculateFontWeight(BuildContext context, double distance) { - if (distance < 0.5) { - return FontWeight.w600; - } else { - return FontWeight.w400; + switch (_level(distance)) { + case 0: return FontWeight.w700; // 选中 + case 1: return FontWeight.w500; // 紧邻 + case 2: return FontWeight.w400; // 近边 + default: return FontWeight.w300; // 远边 } } + /// 大小:中心最大,边缘缩小(产生远近透视感) double calculateFont(BuildContext context, double distance) { - return TTheme.of(context).fontBodyLarge!.size; + final baseSize = TTheme.of(context).fontBodyLarge!.size; + switch (_level(distance)) { + case 0: return baseSize * 1.00; // 100% + case 1: return baseSize * 0.94; // 94% + case 2: return baseSize * 0.88; // 88% + default: return baseSize * 0.82; // ~82% + } + } + + /// 透明度:选中=1.0,其余统一 0.6(仅区分选中与非选中) + double calculateOpacity(double distance) { + switch (_level(distance)) { + case 0: return 1.00; // 选中 + default: return 0.75; // 非选中(统一) + } } } diff --git a/tdesign-component/lib/src/components/picker/t_multi_picker.dart b/tdesign-component/lib/src/components/picker/t_multi_picker.dart deleted file mode 100644 index 43811cc24..000000000 --- a/tdesign-component/lib/src/components/picker/t_multi_picker.dart +++ /dev/null @@ -1,942 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import '../../../tdesign_flutter.dart'; - -import '../../util/context_extension.dart'; -import 'no_wave_behavior.dart'; - -typedef MultiPickerCallback = void Function(List selected); - -/// 项之间无联动的多项选择器 -class TMultiPicker extends StatelessWidget { - /// 选择器标题 - final String? title; - - /// 选择器确认按钮回调 - final MultiPickerCallback? onConfirm; - - /// 选择器取消按钮回调 - final MultiPickerCallback? onCancel; - - /// todo 选择器数据改变时回调 - final MultiPickerCallback? onChange; - - /// 选择器的数据源 - final List> data; - - /// 选择器List的视窗高度,默认200 - final double pickerHeight; - - /// 选择器List视窗中item个数,pickerHeight / pickerItemCount,即item高度 - final int pickerItemCount; - - /// 自定义选择框样式 - final Widget? customSelectWidget; - - /// 右侧按钮文案 - final String? rightText; - - /// 左侧按钮文案 - final String? leftText; - - /// 自定义左侧文案样式 - final TextStyle? leftTextStyle; - - /// 自定义右侧文案样式 - final TextStyle? rightTextStyle; - - /// 自定义中间文案样式 - final TextStyle? centerTextStyle; - - /// 标题高度 - final double? titleHeight; - - /// 顶部填充 - final double? topPadding; - - /// 左边填充 - final double? leftPadding; - - /// 右边填充 - final double? rightPadding; - - /// 标题分割线颜色 - final Color? titleDividerColor; - - /// 背景颜色 - final Color? backgroundColor; - - /// 顶部圆角 - final double? topRadius; - - /// 不同距离自选项计算策略 - final ItemDistanceCalculator? itemDistanceCalculator; - - /// 适配padding - final EdgeInsets? padding; - - /// 若为null表示全部从零开始 - final List? initialIndexes; - - /// 自定义item构建 - final ItemBuilderType? itemBuilder; - - /// 是否显示头部内容 - final bool header; - - static const _pickerTitleHeight = 56.0; - - const TMultiPicker({ - this.title, - required this.onConfirm, - this.onCancel, - this.onChange, - required this.data, - this.pickerHeight = 200, - this.pickerItemCount = 5, - this.initialIndexes, - this.rightText, - this.leftText, - this.leftTextStyle, - this.rightTextStyle, - this.centerTextStyle, - this.titleHeight, - this.topPadding, - this.leftPadding, - this.rightPadding, - this.titleDividerColor, - this.backgroundColor, - this.topRadius, - this.padding, - this.itemDistanceCalculator, - this.customSelectWidget, - this.itemBuilder, - this.header = true, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final dataLength = data.length; - - var indexes = initialIndexes ?? List.generate(dataLength, (i) => 0); - - var controllers = List.generate( - dataLength, - (i) => FixedExtentScrollController(initialItem: indexes[i]), - ); - - final maxWidth = MediaQuery.of(context).size.width; - - return Container( - width: maxWidth, - padding: padding ?? - EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), - decoration: BoxDecoration( - color: backgroundColor ?? TTheme.of(context).bgColorContainer, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - topRadius ?? TTheme.of(context).radiusExtraLarge), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (header) _buildHeader(context, controllers), - Stack( - alignment: Alignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: customSelectWidget ?? - Container( - height: 40, - decoration: BoxDecoration( - color: TTheme.of(context).bgColorSecondaryContainer, - borderRadius: BorderRadius.all( - Radius.circular(TTheme.of(context).radiusDefault)), - ), - ), - ), - // 列表 - Container( - padding: const EdgeInsets.symmetric(horizontal: 32), - height: pickerHeight, - width: maxWidth, - child: Row( - children: List.generate( - dataLength, - (i) => Expanded(child: _buildList(context, i, controllers)), - ), - ), - ), - // 蒙层 - Positioned( - top: 0, - child: IgnorePointer( - ignoring: true, - child: Container( - height: _pickerTitleHeight, - width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - TTheme.of(context).bgColorContainer, - TTheme.of(context).bgColorContainer.withOpacity(0) - ], - ), - ), - ), - ), - ), - Positioned( - bottom: 0, - child: IgnorePointer( - ignoring: true, - child: Container( - height: _pickerTitleHeight, - width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - TTheme.of(context).bgColorContainer, - TTheme.of(context).bgColorContainer.withOpacity(0) - ], - ), - ), - ), - ), - ) - ], - ), - ], - ), - ); - } - - Widget _buildHeader( - BuildContext context, - List controllers, - ) { - final padding = TTheme.of(context).spacer16; - - return Container( - padding: EdgeInsets.only( - left: leftPadding ?? padding, - right: rightPadding ?? padding, - top: topPadding ?? padding, - ), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 0.5, - color: titleDividerColor ?? Colors.transparent, - ), - ), - ), - height: getTitleHeight(), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - /// 左边按钮 - GestureDetector( - onTap: () { - if (onCancel != null) { - onCancel!(controllers - .map((controller) => controller.selectedItem) - .toList()); - } else { - Navigator.of(context).pop(); - } - }, - behavior: HitTestBehavior.opaque, - child: TText( - leftText ?? context.resource.cancel, - style: leftTextStyle ?? - TextStyle( - fontSize: TTheme.of(context).fontBodyLarge!.size, - color: TTheme.of(context).textColorSecondary), - )), - - /// 中间title - Expanded( - child: Center( - child: TText( - title ?? '', - style: centerTextStyle ?? - TextStyle( - fontSize: TTheme.of(context).fontTitleLarge!.size, - fontWeight: FontWeight.w600, - color: TTheme.of(context).textColorPrimary), - ), - ), - ), - - // 右边按钮 - GestureDetector( - onTap: () { - if (onConfirm != null) { - onConfirm!(controllers - .map((controller) => controller.selectedItem) - .toList()); - } - }, - behavior: HitTestBehavior.opaque, - child: TText( - rightText ?? context.resource.confirm, - style: rightTextStyle ?? - TextStyle( - fontSize: TTheme.of(context).fontBodyLarge!.size, - color: TTheme.of(context).brandNormalColor), - ), - ), - ], - ), - ); - } - - double getTitleHeight() => titleHeight ?? _pickerTitleHeight; - - Widget _buildList( - context, - int position, - List controllers, - ) { - var maxWidth = MediaQuery.of(context).size.width; - return MediaQuery.removePadding( - context: context, - removeTop: true, - child: ScrollConfiguration( - behavior: NoWaveBehavior(), - child: ListWheelScrollView.useDelegate( - itemExtent: pickerHeight / pickerItemCount, - diameterRatio: 100, - controller: controllers[position], - physics: const FixedExtentScrollPhysics(), - childDelegate: ListWheelChildBuilderDelegate( - childCount: data[position].length, - builder: (context, index) { - return Container( - key: UniqueKey(), - alignment: Alignment.center, - height: pickerHeight / pickerItemCount, - width: maxWidth, - child: TItemWidget( - colIndex: position, - index: index, - key: UniqueKey(), - itemHeight: pickerHeight / pickerItemCount, - content: data[position][index], - itemDistanceCalculator: itemDistanceCalculator, - fixedExtentScrollController: controllers[position], - itemBuilder: itemBuilder, - )); - })), - )); - } -} - -/// 多项联动选择器 -class TMultiLinkedPicker extends StatefulWidget { - /// 选择器标题 - final String? title; - - /// 选择器确认按钮回调 - final MultiPickerCallback? onConfirm; - - /// 选择器取消按钮回调 - final MultiPickerCallback? onCancel; - - /// todo 选择器数据改变时回调 - final MultiPickerCallback? onChange; - - /// 选中数据 - final List selectedData; - - /// 选择器的数据源 - final Map data; - - /// 最大列数 - final int columnNum; - - /// 选择器List的视窗高度 - final double pickerHeight; - - /// 选择器List视窗中item个数,pickerHeight / pickerItemCount,即item高度 - final int pickerItemCount; - - /// 自定义选择框样式 - final Widget? customSelectWidget; - - /// 右侧按钮文案 - final String? rightText; - - /// 左侧按钮文案 - final String? leftText; - - /// 自定义左侧文案样式 - final TextStyle? leftTextStyle; - - /// 自定义右侧文案样式 - final TextStyle? rightTextStyle; - - /// 自定义中间文案样式 - final TextStyle? centerTextStyle; - - /// 适配padding - final EdgeInsets? padding; - - /// 标题高度 - final double? titleHeight; - - /// 顶部填充 - final double? topPadding; - - /// 左边填充 - final double? leftPadding; - - /// 右边填充 - final double? rightPadding; - - /// 标题分割线颜色 - final Color? titleDividerColor; - - /// 背景颜色 - final Color? backgroundColor; - - /// 顶部圆角 - final double? topRadius; - - /// 不同距离自选项计算策略 - final ItemDistanceCalculator? itemDistanceCalculator; - - /// 自定义item构建 - final ItemBuilderType? itemBuilder; - - /// 是否保留相同选项 - final bool keepSameSelection; - - /// 是否显示头部内容 - final bool header; - - const TMultiLinkedPicker({ - this.title, - required this.onConfirm, - this.onCancel, - this.onChange, - required this.selectedData, - required this.data, - required this.columnNum, - this.pickerHeight = 200, - this.pickerItemCount = 5, - this.customSelectWidget, - this.rightText, - this.leftText, - this.leftTextStyle, - this.rightTextStyle, - this.centerTextStyle, - this.titleHeight, - this.topPadding, - this.leftPadding, - this.rightPadding, - this.titleDividerColor, - this.backgroundColor, - this.topRadius, - this.padding, - this.itemDistanceCalculator, - this.itemBuilder, - this.keepSameSelection = false, - this.header = true, - Key? key, - }) : super(key: key); - - @override - State createState() => _TMultiLinkedPickerState(); -} - -class _TMultiLinkedPickerState extends State { - late MultiLinkedPickerModel model; - - double pickerHeight = 0; - - static const _pickerTitleHeight = 56.0; - - @override - void initState() { - super.initState(); - pickerHeight = widget.pickerHeight; - model = MultiLinkedPickerModel( - data: widget.data, - columnNum: widget.columnNum, - initialData: widget.selectedData, - keepSameSelection: widget.keepSameSelection, - ); - } - - @override - Widget build(BuildContext context) { - final maxWidth = MediaQuery.of(context).size.width; - return Container( - width: maxWidth, - padding: widget.padding ?? - EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), - decoration: BoxDecoration( - color: widget.backgroundColor ?? TTheme.of(context).bgColorContainer, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - widget.topRadius ?? TTheme.of(context).radiusExtraLarge), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.header) _buildHeader(context), - Stack( - alignment: Alignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: widget.customSelectWidget ?? - Container( - height: 40, - decoration: BoxDecoration( - color: TTheme.of(context).bgColorSecondaryContainer, - borderRadius: BorderRadius.all(Radius.circular( - TTheme.of(context).radiusDefault))), - ), - ), - - // 列表 - Container( - padding: const EdgeInsets.symmetric(horizontal: 32), - height: pickerHeight, - width: maxWidth, - child: Row( - children: List.generate( - widget.columnNum, - (i) => Expanded(child: buildList(context, i)), - ), - )), - // 蒙层 - Positioned( - top: 0, - child: IgnorePointer( - ignoring: true, - child: Container( - height: _pickerTitleHeight, - width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - TTheme.of(context).bgColorContainer, - TTheme.of(context).bgColorContainer.withOpacity(0) - ], - ), - ), - ), - ), - ), - Positioned( - bottom: 0, - child: IgnorePointer( - ignoring: true, - child: Container( - height: _pickerTitleHeight, - width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - TTheme.of(context).bgColorContainer, - TTheme.of(context).bgColorContainer.withOpacity(0) - ], - ), - ), - ), - ), - ) - ], - ), - ], - ), - ); - } - - Widget buildList(context, int position) { - // position参数表示这个第几列 - var maxWidth = MediaQuery.of(context).size.width; - return MediaQuery.removePadding( - context: context, - removeTop: true, - child: ScrollConfiguration( - behavior: NoWaveBehavior(), - child: NotificationListener( - onNotification: (ScrollNotification notification) { - // 滚动到底部加载更多 - if (notification is ScrollEndNotification) { - final metrics = notification.metrics; - if (metrics.pixels >= metrics.maxScrollExtent - 10) { - if (model.loadMoreData(position)) { - setState(() {}); - } - } - } - return false; - }, - child: ListWheelScrollView.useDelegate( - itemExtent: pickerHeight / widget.pickerItemCount, - diameterRatio: 100, - controller: model.controllers[position], - physics: const FixedExtentScrollPhysics(), - onSelectedItemChanged: (index) { - if (index >= 0 && index < model.presentData[position].length) { - setState(() { - model.refreshPresentDataAndController( - position, - index, - false, - ); - if (index >= model.presentData[position].length - 5 && - model.hasMoreData[position]) { - if (model.loadMoreData(position)) { - // 延迟一下再刷新,避免连续setState - Future.delayed(const Duration(milliseconds: 50), () { - if (mounted) { - setState(() {}); - } - }); - } - } - - /// todo 通过随机数改变高度来触发UI刷新,这是hack式的解决方案!有待优化! - /// fix https://github.com/flutter/flutter/issues/22999 - pickerHeight = - pickerHeight - Random().nextDouble() / 100000000; - }); - } - }, - childDelegate: ListWheelChildBuilderDelegate( - childCount: model.presentData[position].length + - (model.hasMoreData[position] ? 1 : 0), - builder: (context, index) { - if (index >= model.presentData[position].length) { - // 加载更多指示器 - return Container( - alignment: Alignment.center, - height: pickerHeight / widget.pickerItemCount, - child: Text( - context.resource.loadingWithPoint, - style: TextStyle( - color: TTheme.of(context).textColorPlaceholder, - ), - ), - ); - } - if (index < 0 || - index >= model.presentData[position].length) { - return Container(); - } - return Container( - alignment: Alignment.center, - height: pickerHeight / widget.pickerItemCount, - width: maxWidth, - child: TItemWidget( - colIndex: position, - index: index, - itemHeight: pickerHeight / widget.pickerItemCount, - content: - model.presentData[position][index].toString(), - fixedExtentScrollController: - model.controllers[position], - itemDistanceCalculator: widget.itemDistanceCalculator, - itemBuilder: widget.itemBuilder, - )); - })), - ), - ), - ); - } - - Widget _buildHeader(BuildContext context) { - final padding = TTheme.of(context).spacer16; - - return Container( - padding: EdgeInsets.only( - left: widget.leftPadding ?? padding, - right: widget.rightPadding ?? padding, - top: widget.topPadding ?? padding, - ), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 0.5, - color: widget.titleDividerColor ?? Colors.transparent, - ), - ), - ), - height: getTitleHeight() - 0.5, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - /// 左边按钮 - GestureDetector( - onTap: () { - if (widget.onCancel != null) { - widget.onCancel!(model.selectedData); - } else { - Navigator.of(context).pop(); - } - }, - behavior: HitTestBehavior.opaque, - child: TText( - widget.leftText ?? context.resource.cancel, - style: widget.leftTextStyle ?? - TextStyle( - fontSize: TTheme.of(context).fontBodyLarge!.size, - color: TTheme.of(context).textColorSecondary, - ), - )), - - /// 中间title - Expanded( - child: Center( - child: TText( - widget.title ?? '', - style: widget.centerTextStyle ?? - TextStyle( - fontSize: TTheme.of(context).fontTitleLarge!.size, - fontWeight: FontWeight.w700, - color: TTheme.of(context).textColorPrimary, - ), - ), - ), - ), - - /// 右边按钮 - GestureDetector( - onTap: () { - if (widget.onConfirm != null) { - widget.onConfirm!(model.selectedData); - } - }, - behavior: HitTestBehavior.opaque, - child: TText( - widget.rightText ?? context.resource.confirm, - style: widget.rightTextStyle ?? - TextStyle( - fontSize: TTheme.of(context).fontBodyLarge!.size, - color: TTheme.of(context).brandNormalColor, - ), - ), - ), - ], - ), - ); - } - - double getTitleHeight() => widget.titleHeight ?? _pickerTitleHeight; -} - -class MultiLinkedPickerModel { - /// 占位字符 - static const placeData = ''; - - /// 总的数据 - late Map data; - - /// 选中数据下标 - late List selectedIndexes; - - /// 总列数 - late int columnNum; - - /// 选中数据 - late List selectedData; - - late List controllers = []; - - /// 每一列展示的数据 - late List presentData = []; - - /// 是否保留相同选项 - bool keepSameSelection = false; - - /// 添加一个常量定义每页加载数量 - static const int pageSize = 10; - - /// 每列的当前页码 - late List currentPages; - - /// 每列是否还有更多数据 - late List hasMoreData; - - /// 每列的总数据量 - late List totalCounts; - - MultiLinkedPickerModel({ - required this.data, - required this.columnNum, - required List initialData, - this.keepSameSelection = false, - }) { - selectedData = []; - selectedIndexes = []; - currentPages = List.generate(columnNum, (_) => 0); - hasMoreData = List.generate(columnNum, (_) => true); - totalCounts = List.generate(columnNum, (_) => 0); - for (var i = 0; i < columnNum; ++i) { - if (i >= initialData.length) { - selectedData.add(''); - } else { - selectedData.add(initialData[i]?.toString() ?? ''); - } - selectedIndexes.add(0); - } - _init(initialData); - } - - void _init(List initialData) { - controllers.clear(); - presentData.clear(); - for (var i = 0; i < columnNum; ++i) { - if (i >= presentData.length) { - presentData.add([placeData]); - } - List currentLevelData; - if (i == 0) { - currentLevelData = _getNextLevelDataPaginated(0, 0); - if (currentLevelData.isEmpty) { - currentLevelData = [placeData]; - } - } else { - currentLevelData = _getNextLevelDataPaginated(i, 0); - } - // 处理选中项 - var selectedIndex = currentLevelData.indexOf(selectedData[i]); - if (selectedIndex < 0) { - selectedData[i] = - currentLevelData.isNotEmpty ? currentLevelData.first : placeData; - selectedIndex = 0; - } - selectedIndexes[i] = selectedIndex; - presentData[i] = currentLevelData; - // 创建控制器 - controllers.add(FixedExtentScrollController( - initialItem: selectedIndex.clamp(0, currentLevelData.length - 1))); - } - } - - List _getNextLevelDataPaginated(int level, int page) { - try { - dynamic currentData = data; - for (var i = 0; i < level; i++) { - if (currentData is Map && currentData.containsKey(selectedData[i])) { - currentData = currentData[selectedData[i]]; - } else { - return [placeData]; - } - } - List allData; - if (currentData is Map) { - allData = currentData.keys.toList(); - } else if (currentData is List) { - allData = currentData; - } else { - allData = [currentData?.toString() ?? placeData]; - } - totalCounts[level] = allData.length; - int start = page * pageSize; - int end = start + pageSize; - if (start >= allData.length) { - return []; - } - if (end > allData.length) { - end = allData.length; - } - hasMoreData[level] = end < allData.length; - return allData.sublist(start, end); - } catch (e) { - return [placeData]; - } - } - - bool loadMoreData(int columnIndex) { - if (columnIndex >= columnNum || !hasMoreData[columnIndex]) { - return false; - } - List newData; - int nextPage = currentPages[columnIndex] + 1; - if (columnIndex == 0) { - newData = _getNextLevelDataPaginated(0, nextPage); - } else { - newData = _getNextLevelDataPaginated(columnIndex, nextPage); - } - if (newData.isNotEmpty) { - presentData[columnIndex].addAll(newData); - currentPages[columnIndex] = nextPage; - return true; - } else { - hasMoreData[columnIndex] = false; - } - return false; - } - - /// [position] 变动的列 - /// [selectedIndex] 对应选中的index - /// [jump] 是否需要jumpToItem - void refreshPresentDataAndController( - int position, - int selectedIndex, - bool jump, - ) { - // 严格的边界检查 - if (position >= presentData.length || - selectedIndex >= presentData[position].length || - position >= controllers.length) { - return; - } - selectedIndex = selectedIndex.clamp(0, presentData[position].length - 1); - var selectValue = presentData[position][selectedIndex]; - // 更新选中的数据 - selectedData[position] = selectValue; - selectedIndexes[position] = selectedIndex; - if (jump) { - controllers[position].jumpToItem(selectedIndex); - } - // 检查是否需要预加载更多数据 - if (selectedIndex >= presentData[position].length - 5 && - hasMoreData[position]) { - loadMoreData(position); - } - if (position < columnNum - 1) { - List nextColumnData; - if (presentData[position].length == 1 && - presentData[position].first == placeData) { - nextColumnData = [placeData]; - } else { - nextColumnData = _getNextLevelDataPaginated(position + 1, 0); - currentPages[position + 1] = 0; - hasMoreData[position + 1] = true; - } - if (nextColumnData.isEmpty) { - nextColumnData = [placeData]; - } - while (presentData.length <= position + 1) { - presentData.add([placeData]); - } - presentData[position + 1] = nextColumnData; - while (controllers.length <= position + 1) { - controllers.add(FixedExtentScrollController(initialItem: 0)); - } - refreshPresentDataAndController(position + 1, 0, true); - } - } -} diff --git a/tdesign-component/lib/src/components/picker/t_picker.dart b/tdesign-component/lib/src/components/picker/t_picker.dart index d09bc04cb..8a32bf3c5 100644 --- a/tdesign-component/lib/src/components/picker/t_picker.dart +++ b/tdesign-component/lib/src/components/picker/t_picker.dart @@ -1,239 +1,554 @@ import 'package:flutter/material.dart'; import '../../../tdesign_flutter.dart'; +import 'no_wave_behavior.dart'; +import 't_picker_scroll_physics.dart'; -class TPicker { - /// 显示时间选择器 - static void showDatePicker( - 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, - // 通过弹窗方式打开必须展示header - // bool header = true, - // ItemDistanceCalculator? itemDistanceCalculator, - /// DatePickerModel参数 - bool useYear = true, - bool useMonth = true, - bool useDay = true, - bool useHour = false, - bool useMinute = false, - bool useSecond = false, - bool useWeekDay = false, - List dateStart = const [1970, 1, 1], - List? dateEnd, - List? initialDate, - List Function(DateTypeKey key, List nums)? filterItems, - double pickerHeight = 200, - int pickerItemCount = 5, - bool isTimeUnit = true, - ItemBuilderType? itemBuilder, - Color? barrierColor, - - /// todo 未传参 - Duration duration = const Duration(milliseconds: 100), - }) { - if (dateEnd == null || initialDate == null) { - final now = DateTime.now(); - // 如果未指定结束时间,则取当前时间 - dateEnd ??= [now.year, now.month, now.day]; - initialDate ??= [now.year, now.month, now.day]; - } - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - barrierColor: barrierColor, - enableDrag: false, - builder: (context) { - return TDatePicker( - title: title, - titleHeight: titleHeight, - titleDividerColor: titleDividerColor, - onConfirm: onConfirm, - onCancel: onCancel, - onChange: onChange, - onSelectedItemChanged: onSelectedItemChanged, - leftText: leftText, - leftTextStyle: leftTextStyle, - centerTextStyle: centerTextStyle, - rightText: rightText, - rightTextStyle: rightTextStyle, - padding: padding, - leftPadding: leftPadding, - topPadding: topPadding, - rightPadding: rightPadding, - topRadius: topRadius, - backgroundColor: backgroundColor, - customSelectWidget: customSelectWidget, - // header: header, - model: DatePickerModel( - useYear: useYear, - useMonth: useMonth, - useDay: useDay, - useHour: useHour, - useMinute: useMinute, - useSecond: useSecond, - useWeekDay: useWeekDay, - dateStart: dateStart, - dateEnd: dateEnd!, - dateInitial: initialDate, - filterItems: filterItems, +/// 纯滚轮选择器组件 +/// +/// 数据决定形态: +/// - `List>` → 多列独立选择 +/// - `Map` → 联动选择(Key 必须是 `TPickerOption`) +class TPicker extends StatefulWidget { + const TPicker({ + super.key, + required this.items, + this.initialValue, + this.onChange, + this.onLoad, + this.preloadThreshold = 5, + this.height = 200, + this.itemCount = 5, + this.disabled = false, + }); + + /// 数据源(必填) + final dynamic items; + + /// 初始选中值列表(按 value 匹配) + final List? initialValue; + + /// 值改变回调 + final void Function(TPickerValue)? onChange; + + /// 接近底部时加载回调 + final void Function(TPickerLoadEvent)? onLoad; + + /// 预加载阈值(距底部剩余 N 项时触发),默认 5 + final int preloadThreshold; + + /// 视窗高度,默认 200 + final double height; + + /// 每屏显示 item 数,默认 5 + final int itemCount; + + /// 是否禁用整个选择器(禁止滚动和操作),默认 false + final bool disabled; + + @override + State createState() => _TPickerState(); +} + +class _TPickerState extends State { + late final bool _isLinked = widget.items is Map; + late final List> _columns; + late final List _controllers; + // 联动模式:每层选中的 value 路径 + late final List _selectedPath; + // 联动模式:每层的父级 Map(用于查找子数据) + late final List _mapStack; + // 记录每列上次选中的 index,用于判断滚动方向 + late final List _lastSelectedIndex; + // 标记某列正在动画修正中,防止重复触发 _notifyChange + final Set _animatingCols = {}; + + double get _itemHeight => widget.height / widget.itemCount; + + @override + void initState() { + super.initState(); + if (_isLinked) { + _initLinked(); + } else { + _initColumns(); + } + } + + // ========== 初始化 ========== + + /// 初始化多列独立选择模式 + /// + /// 从 widget.items(List>)中提取各列数据, + /// 根据 widget.initialValue 设置每列的初始选中位置, + /// 并为每列创建 FixedExtentScrollController + void _initColumns() { + _columns = widget.items.cast>(); + _selectedPath = []; + _mapStack = []; + _lastSelectedIndex = []; + + final initValues = widget.initialValue; + _controllers = List.generate(_columns.length, (i) { + var index = 0; + if (initValues != null && i < initValues.length) { + final targetIdx = _columns[i].indexWhere((o) => o.value == initValues[i]); + if (targetIdx >= 0) { + index = targetIdx; + } + } + _lastSelectedIndex.add(index); + return FixedExtentScrollController(initialItem: index); + }); + } + + /// 初始化联动选择模式 + /// + /// 根据 widget.items(Map)递归构建各列数据: + /// 1. 从根 Map 开始,提取第一列选项(Map 的 Key) + /// 2. 根据 widget.initialValue 设置每列的初始选中项 + /// 3. 递归查找子数据(Map 的 Value),构建后续列 + /// 4. 为每列创建 FixedExtentScrollController + /// + /// 数据结构约定: + /// - Map 的 Key 必须是 TPickerOption(或能被转为 TPickerOption) + /// - Map 的 Value 可以是 Map(继续联动)或 List(末级) + void _initLinked() { + final rootMap = widget.items as Map; + _columns = []; + _controllers = []; + _selectedPath = []; + _mapStack = []; + _lastSelectedIndex = []; + + var currentMap = rootMap; + var options = _keysToOptions(currentMap); + if (options.isEmpty) { + return; + } + + _columns.add(options); + final initValues = + widget.initialValue is List ? widget.initialValue! : []; + + for (var depth = 0; depth <= initValues.length; depth++) { + var idx = 0; + if (depth < initValues.length) { + final found = options.indexWhere((o) => o.value == initValues[depth]); + if (found >= 0) { + idx = found; + } + } + + if (_controllers.length <= depth) { + _controllers.add(FixedExtentScrollController(initialItem: idx)); + _lastSelectedIndex.add(idx); + } + if (options.isNotEmpty && idx < options.length) { + _selectedPath.add(options[idx].value); + } + if (depth >= initValues.length) { + break; + } + + final childData = currentMap[options[idx]]; + if (childData == null) { + break; + } + + _mapStack.add(currentMap); + + if (childData is Map) { + currentMap = childData; + options = _keysToOptions(currentMap); + if (options.isNotEmpty) { + _columns.add(options); + } else { + break; + } + } else if (childData is List) { + final leaf = childData.cast(); + if (leaf.isNotEmpty) { + _columns.add(leaf); + } + break; + } + } + } + + // ========== 构建 UI ========== + + @override + Widget build(BuildContext context) { + return Opacity( + opacity: widget.disabled ? 0.5 : 1.0, + child: AbsorbPointer( + absorbing: widget.disabled, + child: SizedBox( + height: widget.height, + width: MediaQuery.of(context).size.width, + child: Stack( + alignment: Alignment.center, + children: [ + Positioned( + top: (widget.height - _itemHeight) / 2, + left: 16, + right: 16, + child: Container( + height: _itemHeight, + decoration: BoxDecoration( + color: TTheme.of(context).bgColorSecondaryContainer, + borderRadius: + BorderRadius.circular(TTheme.of(context).radiusDefault), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + children: [ + for (int i = 0; i < _controllers.length; i++) + Expanded(child: _buildColumn(i)), + ], + ), + ), + ], ), - pickerHeight: pickerHeight, - pickerItemCount: pickerItemCount, - isTimeUnit: isTimeUnit, - itemBuilder: itemBuilder, - ); - }, + ), + ), ); } - /// 显示多级选择器 - static void showMultiPicker( - context, { - String? title, - required MultiPickerCallback? onConfirm, - MultiPickerCallback? onCancel, - required List> data, - double pickerHeight = 200, - int pickerItemCount = 5, - List? 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, - - /// todo 未传参 - Duration duration = const Duration(milliseconds: 100), - Color? barrierColor, - }) { - showModalBottomSheet( + Widget _buildColumn(int colIndex) { + final data = _columns[colIndex]; + if (data.isEmpty) { + return const SizedBox.shrink(); + } + + return MediaQuery.removePadding( context: context, - backgroundColor: Colors.transparent, - barrierColor: barrierColor, - builder: (context) { - return TMultiPicker( - title: title, - onConfirm: onConfirm, - onCancel: onCancel, - data: data, - pickerHeight: pickerHeight, - pickerItemCount: pickerItemCount, - initialIndexes: initialIndexes, - rightText: rightText, - leftText: leftText, - leftTextStyle: leftTextStyle, - rightTextStyle: rightTextStyle, - centerTextStyle: centerTextStyle, - titleHeight: titleHeight, - topPadding: topPadding, - leftPadding: leftPadding, - rightPadding: rightPadding, - titleDividerColor: titleDividerColor, - backgroundColor: backgroundColor, - topRadius: topRadius, - padding: padding, - itemBuilder: itemBuilder, - customSelectWidget: customSelectWidget, - ); - }, + removeTop: true, + child: ScrollConfiguration( + behavior: NoWaveBehavior(), + child: NotificationListener( + onNotification: (notification) => + _onScrollNotification(notification, colIndex, data), + child: ListWheelScrollView.useDelegate( + itemExtent: _itemHeight, + diameterRatio: 3, // 圆柱直径/视窗高度比,值越小弧度越明显(iOS 风格约 2~4) + controller: _controllers[colIndex], + physics: widget.disabled + ? const NeverScrollableScrollPhysics() + : const TPickerScrollPhysics(), + onSelectedItemChanged: widget.disabled + ? null + : (index) => _onItemSelected(colIndex, index, data), + childDelegate: ListWheelChildBuilderDelegate( + childCount: data.length, + builder: (_, index) => TItemWidget( + content: data[index].label, + fixedExtentScrollController: _controllers[colIndex], + colIndex: colIndex, + index: index, + itemHeight: _itemHeight, + disabled: data[index].disabled, + ), + ), + ), + ), + ), ); } - /// 显示多级联动选择器 - // required this.selectedData, - static void showMultiLinkedPicker( - context, { - String? title, - required MultiPickerCallback? onConfirm, - MultiPickerCallback? onCancel, - required List initialData, - required Map data, - required int columnNum, - double pickerHeight = 200, - int pickerItemCount = 5, - 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, - // ItemDistanceCalculator? itemDistanceCalculator, - bool keepSameSelection = false, - Color? barrierColor, - - /// todo 未传参 - Duration duration = const Duration(milliseconds: 100), - }) { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - barrierColor: barrierColor, - builder: (context) { - return TMultiLinkedPicker( - title: title, - onConfirm: onConfirm, - onCancel: onCancel, - selectedData: initialData, - data: data, - columnNum: columnNum, - pickerHeight: pickerHeight, - pickerItemCount: pickerItemCount, - rightText: rightText, - leftText: leftText, - leftTextStyle: leftTextStyle, - rightTextStyle: rightTextStyle, - centerTextStyle: centerTextStyle, - titleHeight: titleHeight, - topPadding: topPadding, - leftPadding: leftPadding, - rightPadding: rightPadding, - titleDividerColor: titleDividerColor, - backgroundColor: backgroundColor, - topRadius: topRadius, - padding: padding, - itemBuilder: itemBuilder, - customSelectWidget: customSelectWidget, - keepSameSelection: keepSameSelection, - // itemDistanceCalculator: itemDistanceCalculator, - ); - }, - ); + // ========== 滚动通知处理 ========== + + /// 统一处理所有滚动通知,实现 disabled 项修正: + /// + /// **修正策略**: + /// 1. Physics 层完全不干预(避免延迟和抖动) + /// 2. 滚动完全停止后,用 animateToItem 平滑修正(有动画、无冲突) + /// 3. 使用 addPostFrameCallback 确保在正确时机执行 + /// 4. 使用 _animatingCols 防止重复触发 + bool _onScrollNotification( + ScrollNotification notification, int col, List data) { + // 只在滚动结束时处理 + if (notification is! ScrollEndNotification) { + return false; + } + + final controller = _controllers[col]; + final currentIndex = controller.selectedItem; + + // 边界检查 + if (currentIndex < 0 || currentIndex >= data.length) { + return false; + } + if (!data[currentIndex].disabled) { + return false; // 已在 enabled 上 → OK + } + + // 双向搜索最近 enabled + final forward = _findNearestEnabled(data, currentIndex, 1); + final backward = _findNearestEnabled(data, currentIndex, -1); + + var target = currentIndex; + if (forward >= 0 && backward >= 0) { + target = (forward - currentIndex).abs() <= (backward - currentIndex).abs() + ? forward + : backward; + } else if (forward >= 0) { + target = forward; + } else if (backward >= 0) { + target = backward; + } else { + return false; // 全部 disabled + } + + // 🔑 关键:在下一帧执行 animateToItem,此时滚动已完全停止 + // 使用 addPostFrameCallback 避免与当前帧的滚动状态冲突 + // 动画时长根据距离动态调整:近距离 200ms,远距离 350ms + if (_animatingCols.contains(col)) { + return false; // 防止重复触发 + } + _animatingCols.add(col); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + final c = _controllers[col]; + + // 再次检查:如果当前已在 enabled 上,无需修正 + final newIndex = c.selectedItem; + if (newIndex >= 0 && newIndex < data.length && !data[newIndex].disabled) { + _animatingCols.remove(col); + return; + } + + final distance = (target - currentIndex).abs(); + final duration = distance <= 2 ? 200 : 350; + c.animateToItem( + target, + duration: Duration(milliseconds: duration), + curve: Curves.easeOutCubic, + ).then((_) { + // 动画完成后再更新状态,确保数据层与 UI 同步 + if (mounted) { + _animatingCols.remove(col); + _lastSelectedIndex[col] = target; + _notifyChange(); + } + }).catchError((_) { + // 动画被中断(如用户再次拖动),清理状态 + _animatingCols.remove(col); + }); + }); + + return false; + } + + // ========== 选择事件回调 ========== + + void _onItemSelected(int col, int index, List data) { + // disabled 项静默忽略(不干预滚动),由 ScrollEnd 统一兜底修正 + if (data[index].disabled) { + return; + } + + _lastSelectedIndex[col] = index; + + if (_isLinked) { + // 联动模式:先刷新后续列(会 setState 并重建新 controller), + // 然后 postFrameCallback 等 rebuild 完成后再通知 onChange, + // 避免"cannot access selectedItem before scroll view is built"异常。 + _refreshLinked(col, index); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _notifyChange(); + _checkPreload(col, index, data.length); + } + }); + } else { + // 非联动模式:直接通知 + _notifyChange(); + _checkPreload(col, index, data.length); + } + } + + /// 从 [start] 出发,沿 [direction] 方向查找最近一个未禁用的索引 + /// + /// [data] - 要搜索的选项列表 + /// [start] - 起始索引 + /// [direction] - 搜索方向(+1 向前,-1 向后) + /// + /// 返回: + /// - 如果找到未禁用的项,返回其索引 + /// - 如果全部禁用或未找到,返回 -1 + int _findNearestEnabled(List data, int start, int direction) { + var i = start + direction; + while (i >= 0 && i < data.length) { + if (!data[i].disabled) { + return i; + } + i += direction; + } + return -1; + } + + /// 联动模式:前列变化 → 刷新后续列 + /// + /// 当用户选中某列的选项时,需要: + /// 1. 更新 _selectedPath(记录每列选中的 value) + /// 2. 清空后续列的数据和控制器 + /// 3. 根据选中项查找子数据,构建新的后续列 + /// 4. 调用 setState 重建 UI + /// + /// [col] - 发生变化的列索引 + /// [newIndex] - 新选中的索引 + void _refreshLinked(int col, int newIndex) { + if (col >= _columns.length - 1) { + return; + } + + final selectedOpt = _columns[col][newIndex]; + _selectedPath.removeRange(col + 1, _selectedPath.length); + _selectedPath.add(selectedOpt.value); + + _columns.removeRange(col + 1, _columns.length); + _controllers.removeRange(col + 1, _controllers.length); + _mapStack.removeRange(col, _mapStack.length); + + final sourceMap = + col < _mapStack.length ? _mapStack.last : widget.items as Map; + final childData = _findChild(sourceMap, selectedOpt.value); + if (childData != null) { + if (childData is List) { + final list = childData.cast(); + if (list.isNotEmpty) { + _columns.add(list); + _controllers.add(FixedExtentScrollController(initialItem: 0)); + _lastSelectedIndex.add(0); + } + } else if (childData is Map) { + final opts = _keysToOptions(childData); + if (opts.isNotEmpty) { + _columns.add(opts); + _controllers.add(FixedExtentScrollController(initialItem: 0)); + _lastSelectedIndex.add(0); + _mapStack.add(childData); + } + } + } + + setState(() {}); + } + + // ========== 回调通知 ========== + + /// 通知外部:当前选中的值已改变 + /// + /// 遍历所有列,收集每列选中的 TPickerOption 和索引, + /// 构造 TPickerValue 对象并通过 widget.onChange 回调通知外部。 + /// + /// **安全策略**: + /// - 使用 clamp 确保索引在有效范围内 + /// - 如果当前索引指向 disabled 项,自动双向搜索最近的 enabled 项 + /// - 如果全部 disabled,保持原索引(由 UI 层负责修正) + void _notifyChange() { + final selectedOptions = []; + final indexes = []; + + for (var i = 0; i < _controllers.length; i++) { + if (_columns[i].isEmpty) { + continue; + } + // Layer 3 安全网:确保永远不会报告 disabled index + var idx = _controllers[i].selectedItem.clamp(0, _columns[i].length - 1); + if (idx < _columns[i].length && _columns[i][idx].disabled) { + // 同时双向搜索,取距离更近的 enabled index + final forward = _findNearestEnabled(_columns[i], idx, 1); + final backward = _findNearestEnabled(_columns[i], idx, -1); + if (forward >= 0 && backward >= 0) { + // 两者都存在,取距离更近的 + idx = (forward - idx).abs() <= (backward - idx).abs() + ? forward + : backward; + } else if (forward >= 0) { + idx = forward; + } else if (backward >= 0) { + idx = backward; + } + // else: 全部 disabled,保持原 index + } + indexes.add(idx); + selectedOptions.add(_columns[i][idx]); + } + + widget.onChange + ?.call(TPickerValue(selectedOptions: selectedOptions, indexes: indexes)); + } + + /// 检查是否需要触发预加载回调 + /// + /// 当 onLoad 回调不为 null,且距离底部剩余项数 ≤ preloadThreshold 时, + /// 触发 widget.onLoad 回调,通知外部加载更多数据。 + /// + /// [col] - 当前列索引 + /// [currentIndex] - 当前选中的索引 + /// [total] - 该列的总数据量 + void _checkPreload(int col, int currentIndex, int total) { + if (widget.onLoad == null) { + return; + } + final remaining = total - currentIndex - 1; + if (remaining <= widget.preloadThreshold && remaining > 0) { + widget.onLoad?.call(TPickerLoadEvent( + column: col, + parentValue: + col > 0 && col <= _selectedPath.length + ? _selectedPath[col - 1] + : null, + displayedCount: total, + remaining: remaining, + )); + } + } + + // ========== 工具方法 ========== + + /// 将 Map 的 Key 转换为 TPickerOption 列表 + /// + /// 如果 Key 已经是 TPickerOption,则直接使用; + /// 否则用 Key 的 toString() 作为 label,Key 作为 value 创建新的 TPickerOption + List _keysToOptions(Map map) { + return [ + for (final key in map.keys) + key is TPickerOption + ? key + : TPickerOption(label: key.toString(), value: key), + ]; + } + + /// 在 Map 中查找指定 value 对应的子数据 + /// + /// [map] - 要搜索的 Map(Key 可能是 TPickerOption 或普通值) + /// [targetValue] - 要查找的目标值(匹配 TPickerOption.value 或 Key 本身) + /// + /// 返回: + /// - 如果找到匹配项,返回对应的子数据(可能是 List 或 Map) + /// - 如果未找到或子数据为 null,返回 null 并打印警告日志 + dynamic _findChild(Map map, dynamic targetValue) { + for (final key in map.keys) { + final kv = key is TPickerOption ? key.value : key; + if (kv == targetValue) { + final child = map[key]; + if (child == null) { + debugPrint('⚠️ TPicker: $targetValue 的子数据为 null'); + } + return child; + } + } + debugPrint('⚠️ TPicker: 在 Map 中未找到 $targetValue'); + return null; } } diff --git a/tdesign-component/lib/src/components/picker/t_picker_option.dart b/tdesign-component/lib/src/components/picker/t_picker_option.dart new file mode 100644 index 000000000..a841d9f26 --- /dev/null +++ b/tdesign-component/lib/src/components/picker/t_picker_option.dart @@ -0,0 +1,44 @@ +import 'package:flutter/foundation.dart'; + +/// 选择器选项 +/// +/// label 用于显示,value 用于 onChange 返回,两者分离 +/// 支持自定义显示(emoji、单位、国际化)同时保持纯净的业务值 +/// +/// ```dart +/// TPickerOption(label: '👨 男性', value: 'M') +/// TPickerOption(label: '18岁', value: 18) +/// TPickerOption(label: '广东省', value: 'GD', disabled: true) +/// ``` +@immutable +class TPickerOption { + const TPickerOption({ + required this.label, + required this.value, + this.disabled = false, + }); + + /// 显示文字(可包含 emoji、单位、国际化等) + final String label; + + /// 实际值(onChange 回调返回此字段) + final dynamic value; + + /// 是否禁用(不可选中/置灰显示),默认 false + final bool disabled; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TPickerOption && + runtimeType == other.runtimeType && + label == other.label && + value == other.value && + disabled == other.disabled; + + @override + int get hashCode => Object.hash(label, value, disabled); + + @override + String toString() => 'TPickerOption($label, $value)'; +} diff --git a/tdesign-component/lib/src/components/picker/t_picker_scroll_physics.dart b/tdesign-component/lib/src/components/picker/t_picker_scroll_physics.dart new file mode 100644 index 000000000..15913c091 --- /dev/null +++ b/tdesign-component/lib/src/components/picker/t_picker_scroll_physics.dart @@ -0,0 +1,21 @@ +import 'package:flutter/widgets.dart'; + +/// TPicker 滚动物理效果 —— disabled 项穿透 +/// +/// 策略:完全不干预滚动物理,让滚动自然结束。 +/// disabled 修正统一由调用方的 [ScrollEndNotification] 处理(jumpToItem 瞬时修正)。 +class TPickerScrollPhysics extends FixedExtentScrollPhysics { + const TPickerScrollPhysics({super.parent}); + + @override + TPickerScrollPhysics applyTo(ScrollPhysics? ancestor) => + TPickerScrollPhysics(parent: buildParent(ancestor)); + + @override + Simulation? createBallisticSimulation( + ScrollMetrics position, double velocity) { + // 完全委托父类,不干预任何滚动物理 + // disabled 修正由 ScrollEndNotification + jumpToItem 处理(瞬时、无抖动) + return super.createBallisticSimulation(position, velocity); + } +} diff --git a/tdesign-component/lib/src/components/picker/t_picker_value.dart b/tdesign-component/lib/src/components/picker/t_picker_value.dart new file mode 100644 index 000000000..82ab432e6 --- /dev/null +++ b/tdesign-component/lib/src/components/picker/t_picker_value.dart @@ -0,0 +1,65 @@ +import 't_picker_option.dart'; + +/// onChange 回调返回的选中信息 +/// +/// 每列返回完整的 [TPickerOption],包含 label、value、disabled 等全部字段。 +/// 调用方可按需取值: +/// ```dart +/// onChange: (v) { +/// // 取显示文本 +/// final labels = v.selectedOptions.map((o) => o.label).join(' / '); +/// // 取业务值 +/// final values = v.selectedOptions.map((o) => o.value).toList(); +/// } +/// ``` +class TPickerValue { + TPickerValue({ + required this.selectedOptions, + required this.indexes, + }); + + /// 每列选中的完整 option(顺序对应列号) + final List selectedOptions; + + /// 每列在当前数据列表中的索引(便捷访问) + final List indexes; + + /// 便捷属性:所有 value 的列表(向后兼容) + List get values => selectedOptions.map((o) => o.value).toList(); + + /// 便捷属性:所有 label 的列表 + List get labels => selectedOptions.map((o) => o.label).toList(); + + @override + String toString() => + 'TPickerValue(labels: $labels, values: $values, indexes: $indexes)'; +} + +/// onLoad 回调参数 — 滚动接近底部时触发 +/// +/// 包含滚动位置信息,用于按需加载更多数据 +class TPickerLoadEvent { + TPickerLoadEvent({ + required this.column, + required this.parentValue, + required this.displayedCount, + required this.remaining, + }); + + /// 当前是第几列(从 0 开始) + final int column; + + /// 该列的父级选中值(第一列为 null) + final dynamic parentValue; + + /// 该列当前已显示的数据量 + final int displayedCount; + + /// 距离底部还有多少项 + final int remaining; + + @override + String toString() => + 'TPickerLoadEvent(col:$column, parent:$parentValue, ' + 'displayed:$displayedCount, remaining:$remaining)'; +} diff --git a/tdesign-component/lib/tdesign_flutter.dart b/tdesign-component/lib/tdesign_flutter.dart index 675a6500d..ea6bd1054 100644 --- a/tdesign-component/lib/tdesign_flutter.dart +++ b/tdesign-component/lib/tdesign_flutter.dart @@ -45,10 +45,10 @@ export 'src/components/message/t_message.dart'; export 'src/components/navbar/t_nav_bar.dart'; export 'src/components/notice_bar/t_notice_bar.dart'; export 'src/components/notice_bar/t_notice_bar_style.dart'; -export 'src/components/picker/t_date_picker.dart'; -export 'src/components/picker/t_item_widget.dart'; -export 'src/components/picker/t_multi_picker.dart'; export 'src/components/picker/t_picker.dart'; +export 'src/components/picker/t_picker_option.dart'; +export 'src/components/picker/t_picker_value.dart'; +export 'src/components/picker/t_item_widget.dart'; export 'src/components/popover/t_popover.dart'; export 'src/components/popover/t_popover_widget.dart'; export 'src/components/popup/t_popup_panel.dart'; diff --git a/tdesign-component/test/flutter_component_test.dart b/tdesign-component/test/flutter_component_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/tdesign-component/test/flutter_component_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tdesign-component/test/t_picker_test.dart b/tdesign-component/test/t_picker_test.dart new file mode 100644 index 000000000..2534f3697 --- /dev/null +++ b/tdesign-component/test/t_picker_test.dart @@ -0,0 +1,429 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +void main() { + group('TPicker 组件测试', () { + /// 测试 1: 多列独立选择 + testWidgets('多列独立选择 - 基础功能', (WidgetTester tester) async { + const testData = [ + [ + TPickerOption(label: '选项1', value: 'v1'), + TPickerOption(label: '选项2', value: 'v2'), + TPickerOption(label: '选项3', value: 'v3'), + ], + [ + TPickerOption(label: 'A', value: 'a'), + TPickerOption(label: 'B', value: 'b'), + ], + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: testData, + onChange: (value) {}, + ), + ), + ), + ); + + // 验证初始状态:2 列 + expect(find.byType(ListWheelScrollView), findsNWidgets(2)); + }); + + /// 测试 2: 联动选择(2级) + testWidgets('联动选择 - 2级联动', (WidgetTester tester) async { + final linkedData = { + const TPickerOption(label: '广东省', value: 'GD'): { + const TPickerOption(label: '深圳市', value: 'SZ'): const [ + TPickerOption(label: '南山区', value: 'NS'), + ], + const TPickerOption(label: '广州市', value: 'GZ'): const [ + TPickerOption(label: '天河区', value: 'TH'), + ], + }, + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: linkedData, + onChange: (value) {}, + ), + ), + ), + ); + + // 验证初始状态:至少 1 列(第一列) + expect(find.byType(ListWheelScrollView), findsAtLeast(1)); + }); + + /// 测试 3: 联动选择(3级) + testWidgets('联动选择 - 3级联动', (WidgetTester tester) async { + final linkedData = { + const TPickerOption(label: '广东省', value: 'GD'): { + const TPickerOption(label: '深圳市', value: 'SZ'): const [ + TPickerOption(label: '南山区', value: 'NS'), + TPickerOption(label: '福田区', value: 'FT'), + ], + const TPickerOption(label: '广州市', value: 'GZ'): const [ + TPickerOption(label: '天河区', value: 'TH'), + TPickerOption(label: '越秀区', value: 'YX'), + ], + }, + const TPickerOption(label: '浙江省', value: 'ZJ'): { + const TPickerOption(label: '杭州市', value: 'HZ'): const [ + TPickerOption(label: '西湖区', value: 'XH'), + ], + }, + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: linkedData, + initialValue: const ['GD'], + onChange: (value) {}, + ), + ), + ), + ); + + // 验证初始状态:应该显示 2 列(省 + 市) + expect(find.byType(ListWheelScrollView), findsAtLeast(2)); + }); + + /// 测试 4: 项级禁用(开头禁用) + testWidgets('项级禁用 - 开头禁用', (WidgetTester tester) async { + final disabledData = const [ + [ + TPickerOption(label: '禁用项', value: 'd1', disabled: true), + TPickerOption(label: '正常项1', value: 'n1'), + TPickerOption(label: '正常项2', value: 'n2'), + ], + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: disabledData, + onChange: (value) {}, + ), + ), + ), + ); + + // 验证组件能正常渲染 + expect(find.byType(TPicker), findsOneWidget); + }); + + /// 测试 5: 项级禁用(中间禁用) + testWidgets('项级禁用 - 中间禁用', (WidgetTester tester) async { + final disabledData = const [ + [ + TPickerOption(label: '选项1', value: 'v1'), + TPickerOption(label: '禁用项', value: 'd1', disabled: true), + TPickerOption(label: '选项3', value: 'v3'), + ], + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: disabledData, + onChange: (value) {}, + ), + ), + ), + ); + + expect(find.byType(TPicker), findsOneWidget); + }); + + /// 测试 6: 项级禁用(结尾禁用) + testWidgets('项级禁用 - 结尾禁用', (WidgetTester tester) async { + final disabledData = const [ + [ + TPickerOption(label: '选项1', value: 'v1'), + TPickerOption(label: '选项2', value: 'v2'), + TPickerOption(label: '禁用项', value: 'd1', disabled: true), + ], + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: disabledData, + onChange: (value) {}, + ), + ), + ), + ); + + expect(find.byType(TPicker), findsOneWidget); + }); + + /// 测试 7: 全局禁用 + testWidgets('全局禁用 - disabled=true', (WidgetTester tester) async { + final testData = const [ + [ + TPickerOption(label: '选项1', value: 'v1'), + TPickerOption(label: '选项2', value: 'v2'), + ], + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: testData, + disabled: true, + onChange: (value) {}, + ), + ), + ), + ); + + // 验证组件能正常渲染且被禁用 + expect(find.byType(TPicker), findsOneWidget); + // 验证 AbsorbPointer 存在(禁用时 absorbing=true) + expect(find.byType(AbsorbPointer), findsAtLeast(1)); + }); + + /// 测试 8: 初始值设置 + testWidgets('初始值设置 - 单列', (WidgetTester tester) async { + final testData = const [ + [ + TPickerOption(label: '选项1', value: 'v1'), + TPickerOption(label: '选项2', value: 'v2'), + TPickerOption(label: '选项3', value: 'v3'), + ], + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: testData, + initialValue: const ['v2'], + onChange: (value) {}, + ), + ), + ), + ); + + expect(find.byType(TPicker), findsOneWidget); + }); + + /// 测试 9: 初始值设置(联动) + testWidgets('初始值设置 - 联动', (WidgetTester tester) async { + final linkedData = { + const TPickerOption(label: '广东省', value: 'GD'): { + const TPickerOption(label: '深圳市', value: 'SZ'): const [ + TPickerOption(label: '南山区', value: 'NS'), + ], + }, + const TPickerOption(label: '浙江省', value: 'ZJ'): { + const TPickerOption(label: '杭州市', value: 'HZ'): const [ + TPickerOption(label: '西湖区', value: 'XH'), + ], + }, + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: linkedData, + initialValue: const ['GD', 'SZ'], + onChange: (value) {}, + ), + ), + ), + ); + + expect(find.byType(TPicker), findsOneWidget); + }); + + /// 测试 10: onChange 回调 + testWidgets('onChange 回调 - 触发验证', (WidgetTester tester) async { + dynamic capturedValue; + final testData = const [ + [ + TPickerOption(label: '选项1', value: 'v1'), + TPickerOption(label: '选项2', value: 'v2'), + ], + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: testData, + onChange: (value) { + capturedValue = value; + }, + ), + ), + ), + ); + + expect(find.byType(TPicker), findsOneWidget); + }); + + /// 测试 11: onLoad 回调(按需加载) + testWidgets('onLoad 回调 - 按需加载', (WidgetTester tester) async { + final lazyData = [ + List.generate( + 20, + (i) => TPickerOption(label: '选项 $i', value: 'opt_$i'), + ), + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: lazyData, + preloadThreshold: 5, + onLoad: (event) {}, + onChange: (value) {}, + ), + ), + ), + ); + + expect(find.byType(TPicker), findsOneWidget); + }); + + /// 测试 12: TPickerOption 类型测试 + test('TPickerOption - 基础属性', () { + const option = TPickerOption( + label: '测试', + value: 'test', + disabled: true, + ); + + expect(option.label, '测试'); + expect(option.value, 'test'); + expect(option.disabled, true); + }); + + /// 测试 13: TPickerValue 类型测试 + test('TPickerValue - 便捷属性', () { + final options = const [ + TPickerOption(label: 'A', value: 1), + TPickerOption(label: 'B', value: 2), + ]; + const indexes = [0, 1]; + + final value = TPickerValue(selectedOptions: options, indexes: indexes); + + expect(value.selectedOptions, options); + expect(value.indexes, indexes); + expect(value.values, const [1, 2]); + expect(value.labels, const ['A', 'B']); + }); + + /// 测试 14: TPickerLoadEvent 类型测试 + test('TPickerLoadEvent - 基础属性', () { + final event = TPickerLoadEvent( + column: 0, + parentValue: null, + displayedCount: 10, + remaining: 5, + ); + + expect(event.column, 0); + expect(event.parentValue, null); + expect(event.displayedCount, 10); + expect(event.remaining, 5); + }); + + /// 测试 15: 空数据处理 + testWidgets('空数据处理 - 单列空列表', (WidgetTester tester) async { + final emptyData = const [ + [], + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: emptyData, + onChange: (value) {}, + ), + ), + ), + ); + + expect(find.byType(TPicker), findsOneWidget); + }); + + /// 测试 16: 参数验证 - height 和 itemCount + testWidgets('参数验证 - height 和 itemCount', (WidgetTester tester) async { + final testData = const [ + [ + TPickerOption(label: '选项1', value: 'v1'), + TPickerOption(label: '选项2', value: 'v2'), + ], + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: testData, + height: 300, + itemCount: 3, + onChange: (value) {}, + ), + ), + ), + ); + + expect(find.byType(TPicker), findsOneWidget); + }); + + /// 测试 17: 多列不同长度 + testWidgets('多列不同长度', (WidgetTester tester) async { + final testData = const [ + [ + TPickerOption(label: 'A1', value: 'a1'), + TPickerOption(label: 'A2', value: 'a2'), + TPickerOption(label: 'A3', value: 'a3'), + ], + [ + TPickerOption(label: 'B1', value: 'b1'), + ], + [ + TPickerOption(label: 'C1', value: 'c1'), + TPickerOption(label: 'C2', value: 'c2'), + TPickerOption(label: 'C3', value: 'c3'), + TPickerOption(label: 'C4', value: 'c4'), + ], + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TPicker( + items: testData, + onChange: (value) {}, + ), + ), + ), + ); + + // 验证 3 列 + expect(find.byType(ListWheelScrollView), findsNWidgets(3)); + }); + }); +} diff --git a/tdesign-site/src/picker/README.md b/tdesign-site/src/picker/README.md index b93f2712e..ac169a318 100644 --- a/tdesign-site/src/picker/README.md +++ b/tdesign-site/src/picker/README.md @@ -16,284 +16,304 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ## 代码演示 -[td_picker_page.dart](https://github.com/Tencent/tdesign-flutter/blob/main/tdesign-component/example/lib/page/td_picker_page.dart) +[td_picker_page.dart](https://github.com/Tencent/tdesign-flutter/blob/main/tdesign-component/example/lib/page/t_picker_page.dart) ### 1 组件类型 -基础选择器--地区 - +#### 单列选择 +
-  Widget buildArea(BuildContext context) {
-    const title = '选择地区';
-    return TCell(
-      title: title,
-      note: selected_1.isEmpty ? '请选择' : selected_1,
-      arrow: true,
-      onClick: (click) {
-        TPicker.showMultiPicker(
+  Widget buildSingleColumn(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text('选中城市: ${selectedCity.isEmpty ? "未选择" : selectedCity}',
+            style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)),
+        SizedBox(height: 8),
+        _pickerCard(
           context,
-          title: title,
-          onConfirm: (selected) {
-            setState(() {
-              selected_1 = '${data_1[selected[0]]}';
-            });
-            Navigator.of(context).pop();
-          },
-          data: [data_1],
-        );
-      },
+          child: TPicker(items: cityData,
+              onChange: (v) => setState(() => selectedCity = v.labels.first)),
+        ),
+      ],
     );
   }
- -基础选择器--时间 - + +#### 时间选择(时分秒) +
-  Widget buildTime(BuildContext context) {
-    const title = '选择时间';
-    return TCell(
-      title: title,
-      note: selected_2.isEmpty ? '请选择' : selected_2,
-      arrow: true,
-      onClick: (click) {
-        TPicker.showMultiPicker(
+  // 数据定义:24小时 × 60分钟 × 60秒
+  final timeData = [
+    [for (int i = 0; i < 24; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}时', value: i)],
+    [for (int i = 0; i < 60; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}分', value: i)],
+    [for (int i = 0; i < 60; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}秒', value: i)],
+  ];
+
+  Widget buildTimeSelect(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text('选中时间: ${selectedTime.isEmpty ? "未选择" : selectedTime}',
+            style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)),
+        SizedBox(height: 8),
+        _pickerCard(
           context,
-          title: title,
-          onConfirm: (selected) {
-            print('selected ${selected}');
-            setState(() {
-              selected_2 =
-                  '${data_2[0][selected[0]]} ${data_2[1][selected[1]]}';
-            });
-            Navigator.of(context).pop();
-          },
-          data: data_2,
-        );
-      },
+          child: TPicker(items: timeData, itemCount: 5,
+              onChange: (v) => setState(() =>
+                  selectedTime = '${v.values[0]}:${v.values[1].toString().padLeft(2, '0')}:${v.values[2].toString().padLeft(2, '0')}')),
+        ),
+      ],
     );
   }
- -基础选择器--地区--联动 - + +#### 联动选择(省市区) +
-  Widget buildMultiArea(BuildContext context) {
-    const title = '选择地区';
-    return TCell(
-      title: title,
-      note: selected_3.isEmpty ? '请选择' : selected_3,
-      arrow: true,
-      onClick: (click) {
-        TPicker.showMultiLinkedPicker(
+  Widget buildLinked(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text('选中地区: ${selectedLinked.isEmpty ? "未选择" : selectedLinked}',
+            style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)),
+        SizedBox(height: 8),
+        _pickerCard(
           context,
-          title: title,
-          onConfirm: (selected) {
-            setState(() {
-              selected_3 = '${selected[0]} ${selected[1]} ${selected[2]}';
-            });
-            Navigator.of(context).pop();
-          },
-          data: dataTest,
-          columnNum: 3,
-          initialData: ['浙江省', '杭州市', '西湖区'],
-        );
-      },
+          child: TPicker(items: linkedData, initialValue: ['GD'],
+              onChange: (v) => setState(() => selectedLinked = v.labels.join(' / '))),
+        ),
+      ],
     );
   }
- -### 1 组件样式 -带标题选择器 - + +### 2 禁用状态 + +#### 项级 disabled(部分选项不可选) +
-  Widget buildAreaWithTitle(BuildContext context) {
-    const title = '选择地区';
-    return TCell(
-      title: title,
-      note: selected_4.isEmpty ? '请选择' : selected_4,
-      arrow: true,
-      onClick: (click) {
-        TPicker.showMultiPicker(
+  Widget buildItemDisabled(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text('选中: ${selectedItemDisabled.isEmpty ? "未选择" : selectedItemDisabled}',
+            style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)),
+        SizedBox(height: 4),
+        Text('提示: 标灰的选项不可选(如 <16岁、40岁+、50岁+、保密)',
+            style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder)),
+        SizedBox(height: 8),
+        _pickerCard(
           context,
-          title: '带标题选择器',
-          onConfirm: (selected) {
-            setState(() {
-              selected_4 = '${data_1[selected[0]]}';
-            });
-            Navigator.of(context).pop();
-          },
-          data: [data_1],
-        );
-      },
+          child: TPicker(items: itemDisabledData, initialValue: ['M', 25],
+              onChange: (v) => setState(() =>
+                  selectedItemDisabled = '${v.labels.first} ${v.labels.last}')),
+        ),
+      ],
+    );
+  }
+ +
+ + +#### 全局 disabled(整组不可操作) + + + +
+  Widget buildGlobalDisabled(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Row(
+          children: [
+            Switch(
+              value: globalDisabled,
+              onChanged: (v) => setState(() => globalDisabled = v),
+            ),
+            SizedBox(width: 8),
+            Text(globalDisabled ? '已禁用' : '已启用',
+                style: TextStyle(
+                    fontSize: 14,
+                    color: globalDisabled
+                        ? TTheme.of(context).errorNormalColor
+                        : TTheme.of(context).successNormalColor)),
+          ],
+        ),
+        SizedBox(height: 8),
+        _pickerCard(
+          context,
+          child: TPicker(items: cityData, initialValue: ['GZ'],
+              onChange: (v) => debugPrint('选中: $v'),
+              disabled: globalDisabled),
+        ),
+        SizedBox(height: 4),
+        Text('切换开关可控制整个选择器的禁用/启用状态',
+            style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder)),
+      ],
     );
   }
- -无标题选择器 - + +### 3 弹窗模式(TPopup) + +> 弹窗模式下,`onChange` 仅用于记录临时选中值,点击「确认」按钮后才正式更新显示。 + +#### 弹窗-联动选择(省市区) +
-  Widget buildAreaWithoutTitle(BuildContext context) {
+  Widget buildPopupLinked(BuildContext context) {
     return TCell(
-      title: '选择地区',
-      note: selected_5.isEmpty ? '请选择' : selected_5,
+      title: '弹窗-联动选择(省市区)',
+      note: selectedLinked.isEmpty ? '请选择' : selectedLinked,
       arrow: true,
-      onClick: (click) {
-        TPicker.showMultiPicker(
+      onClick: (_) {
+        _showPickerPopup(
           context,
-          // 不传或传空字符串、null,则不显示标题
-          // title: '',
-          onConfirm: (selected) {
-            setState(() {
-              selected_5 = '${data_1[selected[0]]}';
-            });
-            Navigator.of(context).pop();
-          },
-          data: [data_1],
+          title: '请选择地区',
+          picker: TPicker(
+            items: linkedData,
+            initialValue: selectedLinked.isNotEmpty
+                ? selectedLinked.split(' / ')
+                : ['GD'],
+            onChange: (v) => setState(() => _popupLinkedTemp = v.labels.join(' / ')),
+          ),
+          onConfirm: () => setState(() => selectedLinked = _popupLinkedTemp),
         );
       },
     );
   }
- -不使用弹窗、不带顶部内容 - + +#### 弹窗-多列选择(性别/偏好) +
-  Widget buildWithoutHeader(BuildContext context) {
-    return TMultiPicker(
-      /// 不显示header内容
-      header: false,
-      /// todo onChange
-      onConfirm: (selected) {
-        setState(() {
-          selected_5 = '${data_1[selected[0]]}';
-        });
-        Navigator.of(context).pop();
+  // 数据定义
+  final preferenceData = [
+    [
+      TPickerOption(label: '男', value: 'M'),
+      TPickerOption(label: '女', value: 'F'),
+      TPickerOption(label: '其他', value: 'O'),
+    ],
+    [
+      TPickerOption(label: '科技', value: 'tech'),
+      TPickerOption(label: '运动', value: 'sport'),
+      TPickerOption(label: '音乐', value: 'music'),
+      TPickerOption(label: '阅读', value: 'book'),
+      TPickerOption(label: '旅行', value: 'travel'),
+      TPickerOption(label: '美食', value: 'food'),
+    ],
+  ];
+
+  Widget buildPopupMultiColumn(BuildContext context) {
+    return TCell(
+      title: '弹窗-多列选择(性别/偏好)',
+      note: selectedPreference.isEmpty ? '请选择' : selectedPreference,
+      arrow: true,
+      onClick: (_) {
+        _showPickerPopup(
+          context,
+          title: '选择性别和偏好',
+          picker: TPicker(
+            items: preferenceData,
+            initialValue: selectedPreference.isNotEmpty
+                ? selectedPreference.split(' ')
+                : ['M', 'tech'],
+            onChange: (v) => setState(() =>
+                _popupMultiColTemp = '${v.labels.first} ${v.labels.last}'),
+          ),
+          onConfirm: () => setState(() => selectedPreference = _popupMultiColTemp),
+        );
       },
-      data: [data_1],
     );
   }
- ## API -### TMultiPicker +### TPicker #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| backgroundColor | Color? | - | 背景颜色 | -| centerTextStyle | TextStyle? | - | 自定义中间文案样式 | -| customSelectWidget | Widget? | - | 自定义选择框样式 | -| data | Map | - | 总的数据 | -| header | bool | true | 是否显示头部内容 | -| initialIndexes | List? | - | 若为null表示全部从零开始 | -| itemBuilder | ItemBuilderType? | - | 自定义item构建 | -| itemDistanceCalculator | ItemDistanceCalculator? | - | 不同距离自选项计算策略 | -| key | | - | | -| leftPadding | double? | - | 左边填充 | -| leftText | String? | - | 左侧按钮文案 | -| leftTextStyle | TextStyle? | - | 自定义左侧文案样式 | -| onCancel | MultiPickerCallback? | - | 选择器取消按钮回调 | -| onChange | MultiPickerCallback? | - | todo 选择器数据改变时回调 | -| onConfirm | MultiPickerCallback? | - | 选择器确认按钮回调 | -| padding | EdgeInsets? | - | 适配padding | -| pickerHeight | double | 200 | | -| pickerItemCount | int | 5 | 选择器List视窗中item个数,pickerHeight / pickerItemCount,即item高度 | -| rightPadding | double? | - | 右边填充 | -| rightText | String? | - | 右侧按钮文案 | -| rightTextStyle | TextStyle? | - | 自定义右侧文案样式 | -| title | String? | - | 选择器标题 | -| titleDividerColor | Color? | - | 标题分割线颜色 | -| titleHeight | double? | - | 标题高度 | -| topPadding | double? | - | 顶部填充 | -| topRadius | double? | - | 顶部圆角 | - -``` -``` - -### TMultiLinkedPicker +| items | dynamic | - | 数据源(必填):`List>` 多列独立 / `Map` 联动选择 | +| initialValue | List? | - | 初始选中值列表(按 value 匹配) | +| onChange | void Function(TPickerValue)? | - | 值改变回调,返回 `TPickerValue`(含 selectedOptions、indexes、values/labels 便捷属性) | +| onLoad | void Function(TPickerLoadEvent)? | - | 接近底部时加载回调(用于无限滚动) | +| preloadThreshold | int | 5 | 预加载阈值(距底部剩余 N 项时触发) | +| height | double | 200 | 视窗高度 | +| itemCount | int | 5 | 每屏显示 item 数量 | +| disabled | bool | false | 是否禁用整个选择器 | + +### TPickerOption #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| backgroundColor | Color? | - | 背景颜色 | -| centerTextStyle | TextStyle? | - | 自定义中间文案样式 | -| columnNum | int | - | 总列数 | -| customSelectWidget | Widget? | - | 自定义选择框样式 | -| data | Map | - | 总的数据 | -| header | bool | true | 是否显示头部内容 | -| itemBuilder | ItemBuilderType? | - | 自定义item构建 | -| itemDistanceCalculator | ItemDistanceCalculator? | - | 不同距离自选项计算策略 | -| keepSameSelection | bool | false | 是否保留相同选项 | -| key | | - | | -| leftPadding | double? | - | 左边填充 | -| leftText | String? | - | 左侧按钮文案 | -| leftTextStyle | TextStyle? | - | 自定义左侧文案样式 | -| onCancel | MultiPickerCallback? | - | 选择器取消按钮回调 | -| onChange | MultiPickerCallback? | - | todo 选择器数据改变时回调 | -| onConfirm | MultiPickerCallback? | - | 选择器确认按钮回调 | -| padding | EdgeInsets? | - | 适配padding | -| pickerHeight | double | 200 | | -| pickerItemCount | int | 5 | 选择器List视窗中item个数,pickerHeight / pickerItemCount,即item高度 | -| rightPadding | double? | - | 右边填充 | -| rightText | String? | - | 右侧按钮文案 | -| rightTextStyle | TextStyle? | - | 自定义右侧文案样式 | -| selectedData | List | - | 选中数据 | -| title | String? | - | 选择器标题 | -| titleDividerColor | Color? | - | 标题分割线颜色 | -| titleHeight | double? | - | 标题高度 | -| topPadding | double? | - | 顶部填充 | -| topRadius | double? | - | 顶部圆角 | - -``` -``` - -### MultiLinkedPickerModel -#### 默认构造方法 +| label | String (required) | - | 显示文字(可包含 emoji、单位等) | +| value | dynamic (required) | - | 实际值(onChange 回调返回此字段) | +| disabled | bool | false | 是否禁用(不可选中) | -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| columnNum | int | - | 总列数 | -| data | Map | - | 总的数据 | -| initialData | | - | | -| keepSameSelection | bool | false | 是否保留相同选项 | +#### 使用示例 -``` +```dart +TPickerOption(label: '👨 男性', value: 'M') +TPickerOption(label: '18岁', value: 18) +TPickerOption(label: '广东省', value: 'GD', disabled: true) ``` -### TPicker +### TPickerValue +#### onChange 回调返回对象 -#### 静态方法 +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| selectedOptions | List\ | 每列选中的完整 option(顺序对应列号) | +| indexes | List\ | 每列在当前数据列表中的索引 | +| values (getter) | List\ | 所有 value 的便捷列表 | +| labels (getter) | List\ | 所有 label 的便捷列表 | -| 名称 | 返回类型 | 参数 | 说明 | -| --- | --- | --- | --- | -| 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 dateStart, List? dateEnd, List? initialDate, List Function(DateTypeKey key, List 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, | 显示多级联动选择器 | -| showMultiPicker | | required null context, String? title, required MultiPickerCallback? onConfirm, MultiPickerCallback? onCancel, required List> data, double pickerHeight, int pickerItemCount, List? 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, | 显示多级选择器 | +#### 使用示例 + +```dart +TPicker( + items: data, + onChange: (v) { + // 显示文本:v.labels.join(' / ') + // 业务值:v.values + // 完整选项:v.selectedOptions + }, +) +``` +### TPickerLoadEvent +#### onLoad 回调参数 - \ No newline at end of file +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| column | int | 当前列索引(从 0 开始) | +| parentValue | dynamic | 该列父级选中值(第一列为 null) | +| displayedCount | int | 该列当前已显示的数据量 | +| remaining | int | 距离底部还有多少项 |