diff --git a/tdesign-component/demo_tool/all_build.sh b/tdesign-component/demo_tool/all_build.sh index 00be18b3d..b6725c733 100644 --- a/tdesign-component/demo_tool/all_build.sh +++ b/tdesign-component/demo_tool/all_build.sh @@ -19,6 +19,7 @@ # drawer ./bin/demo_tool generate --folder ../lib/src/components/drawer --name TDDrawer,TDDrawerItem,TDDrawerStyle --folder-name drawer --output ../example/assets/api/ --only-api --get-comments # indexes +./bin/demo_tool generate --folder ../lib/src/components/indexes --name TDIndexes,TDIndexesAnchor,TDIndexesList --folder-name indexes --output ../example/assets/api/ --only-api --get-comments # navbar ./bin/demo_tool generate --file ../lib/src/components/navbar/td_nav_bar.dart --name TDNavBar,TDNavBarItem, --folder-name navbar --output ../example/assets/api/ --only-api # sidebar @@ -33,6 +34,7 @@ # 输入 # calendar +./bin/demo_tool generate --folder ../lib/src/components/calendar --name TDCalendar,TDCalendarPopup,TDCalendarStyle --folder-name calendar --output ../example/assets/api/ --only-api # cascader ./bin/demo_tool generate --folder ../lib/src/components/cascader --name TDMultiCascader --folder-name cascader --output ../example/assets/api/ --only-api @@ -70,8 +72,8 @@ ./bin/demo_tool generate --file ../lib/src/components/badge/td_badge.dart --name TDBadge --folder-name badge --output ../example/assets/api/ --only-api # cell ./bin/demo_tool generate --folder ../lib/src/components/cell --name TDCell,TDCellGroup,TDCellStyle --folder-name cell --output ../example/assets/api/ --only-api --get-comments -# countDown -./bin/demo_tool generate --folder ../lib/src/components/count_down --name TDCountDown,TDCountDownController,TDCountDownStyle --folder-name count-down --output ../example/assets/api/ --only-api --get-comments +# timeCounter +./bin/demo_tool generate --folder ../lib/src/components/time_counter --name TDTimeCounter,TDTimeCounterController,TDTimeCounterStyle --folder-name time-counter --output ../example/assets/api/ --only-api --get-comments # collapse ./bin/demo_tool generate --folder ../lib/src/components/collapse --name TDCollapse --folder-name collapse --output ../example/assets/api/ --only-api --get-comments @@ -104,6 +106,7 @@ ./bin/demo_tool generate --file ../lib/src/components/loading/td_loading.dart --name TDLoading --folder-name loading --output ../example/assets/api/ --only-api # message # noticeBar +./bin/demo_tool generate --file ../lib/src/components/notice_bar --name TDNoticeBar,TDNoticeBarStyle --folder-name notice-bar --output ../example/assets/api/ --only-api --get-comments # overlay # popup ./bin/demo_tool generate --folder ../lib/src/components/popup --name TDSlidePopupRoute,TDPopupBottomDisplayPanel,TDPopupBottomConfirmPanel,TDPopupCenterPanel --folder-name popup --output ../example/assets/api/ --only-api --get-comments diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md new file mode 100644 index 000000000..a53ab6e46 --- /dev/null +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -0,0 +1,68 @@ +## API +### TDCalendarStyle +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| decoration | | - | | +| titleStyle | TextStyle? | - | header区域 [TDCalendar.title]的样式 | +| titleMaxLine | int? | - | header区域 [TDCalendar.title]的行数 | +| titleCloseColor | Color? | - | header区域 关闭图标的颜色 | +| weekdayStyle | TextStyle? | - | header区域 周 文字样式 | +| monthTitleStyle | TextStyle? | - | body区域 年月文字样式 | +| cellStyle | TextStyle? | - | 日期样式 | +| centreColor | Color? | - | 日期范围内背景样式 | +| cellDecoration | BoxDecoration? | - | 日期decoration | +| cellPrefixStyle | TextStyle? | - | 日期前面的字符串的样式 | +| cellSuffixStyle | TextStyle? | - | 日期后面的字符串的样式 | + + +#### 工厂构造方法 + +| 名称 | 说明 | +| --- | --- | +| TDCalendarStyle.generateStyle | 生成默认样式 | +| TDCalendarStyle.cellStyle | 日期样式 | + +``` +``` + ### TDCalendar +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| key | | - | | +| firstDayOfWeek | int? | 0 | 第一天从星期几开始,默认 0 = 周日 | +| format | CalendarFormat? | - | 用于格式化日期的函数,可定义日期前后的显示内容和日期样式 | +| maxDate | int? | - | 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认半年后 | +| minDate | int? | - | 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认今天 | +| title | String? | - | 标题 | +| titleWidget | Widget? | - | 标题组件 | +| type | CalendarType? | CalendarType.single | 日历的选择类型,single = 单选;multiple = 多选; range = 区间选择 | +| value | List? | - | 当前选择的日期(fromMillisecondsSinceEpoch),不传则默认今天,当 type = single 时数组长度为1 | +| displayFormat | String? | 'year month' | 年月显示格式,`year`表示年,`month`表示月,如`year month`表示年在前、月在后、中间隔一个空格 | +| cellHeight | double? | 60 | 日期高度 | +| height | double? | - | 高度 | +| width | double? | - | 宽度 | +| style | TDCalendarStyle? | - | 自定义样式 | +| onChange | void Function(List value)? | - | 选中值变化时触发 | +| onCellClick | void Function(int value, DateSelectType type, TDate tdate)? | - | 点击日期时触发 | +| onCellLongPress | void Function(int value, DateSelectType type, TDate tdate)? | - | 长安日期时触发 | +| onHeanderClick | void Function(int index, String week)? | - | 点击周时触发 | + +``` +``` + ### TDCalendarPopup +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | context | 上下文 | +| top | double? | - | 距离顶部的距离 | +| autoClose | bool? | true | 自动关闭;在点击关闭按钮、确认按钮、遮罩层时自动关闭 | +| confirmBtn | Widget? | - | 自定义确认按钮 | +| visible | bool? | - | 默认是否显示日历 | +| onClose | VoidCallback? | - | 关闭时触发 | +| onConfirm | void Function(List value)? | - | 点击确认按钮时触发 | +| builder | CalendarBuilder? | - | 控件构建器,优先级高于[child] | +| child | TDCalendar? | - | 日历控件 | diff --git a/tdesign-component/example/assets/api/dropdown-menu_api.md b/tdesign-component/example/assets/api/dropdown-menu_api.md index d70cb0638..c4e98f3c7 100644 --- a/tdesign-component/example/assets/api/dropdown-menu_api.md +++ b/tdesign-component/example/assets/api/dropdown-menu_api.md @@ -15,14 +15,18 @@ | showOverlay | bool? | true | 是否显示遮罩层 | | isScrollable | bool | false | 是否开启滚动列表 | | arrowIcon | IconData? | - | 自定义箭头图标 | +| labelBuilder | LabelBuilder? | - | 自定义标签内容 | | onMenuOpened | ValueChanged? | - | 展开菜单事件 | | onMenuClosed | ValueChanged? | - | 关闭菜单事件 | +| width | double? | - | menu的宽度 | +| height | double? | 48 | menu的高度 | +| tabBarAlign | MainAxisAlignment? | MainAxisAlignment.center | [TDDropdownItem.label]和[arrowIcon]/[TDDropdownItem.arrowIcon]的对齐方式 | ``` ``` ### TDDropdownItem #### 简介 -下拉菜单 +下拉菜单内容 #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | @@ -30,6 +34,7 @@ | key | | - | | | disabled | bool? | false | 是否禁用 | | label | String? | - | 标题 | +| arrowIcon | IconData? | - | 自定义箭头图标 | | multiple | bool? | false | 是否多选 | | options | List? | const [] | 选项数据 | | builder | TDDropdownItemContentBuilder? | - | 完全自定义展示内容 | @@ -39,6 +44,8 @@ | onReset | VoidCallback? | - | 点击重置时触发 | | minHeight | double? | - | 内容最小高度 | | maxHeight | double? | - | 内容最大高度 | +| tabBarWidth | double? | - | 该item在menu上的宽度,仅在[TDDropdownMenu.isScrollable]为true时有效 | +| tabBarAlign | MainAxisAlignment? | - | [label]和[arrowIcon]/[TDDropdownMenu.arrowIcon]的对齐方式 | ``` ``` diff --git a/tdesign-component/example/assets/api/indexes_api.md b/tdesign-component/example/assets/api/indexes_api.md new file mode 100644 index 000000000..2bfc1f540 --- /dev/null +++ b/tdesign-component/example/assets/api/indexes_api.md @@ -0,0 +1,53 @@ +## API +### TDIndexes +#### 简介 +索引 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| key | | - | | +| indexList | List? | - | 索引字符列表。不传默认 A-Z | +| indexListMaxHeight | double? | 0.8 | 索引列表最大高度(父容器高度的百分比,默认0.8) | +| sticky | bool? | true | 锚点是否吸顶 | +| stickyOffset | double? | 0 | 锚点吸顶时与顶部的距离 | +| capsuleTheme | bool? | false | 锚点是否为胶囊式样式 | +| reverse | bool? | false | 反方向滚动置顶 | +| scrollController | ScrollController? | - | 滚动控制器 | +| onChange | void Function(String index)? | - | 索引发生变更时触发事件 | +| onSelect | void Function(String index)? | - | 点击侧边栏时触发事件 | +| builderContent | Widget? Function(BuildContext context, String index) | - | 内容自定义构建 | +| builderAnchor | Widget? Function(BuildContext context, String index, bool isPinnedToTop)? | - | 锚点自定义构建 | +| builderIndex | Widget Function(BuildContext context, String index, bool isActive)? | - | 索引文本自定义构建,包括索引激活左侧提示 | + +``` +``` + ### TDIndexesList +#### 简介 +索引 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| key | | - | | +| indexList | List | - | 索引字符列表。不传默认 A-Z | +| indexListMaxHeight | double | 0.8 | 索引列表最大高度(父容器高度的百分比,默认0.8) | +| activeIndex | ValueNotifier | - | 选中索引 | +| onSelect | void Function(String index, bool isUp) | - | 点击侧边栏时触发事件 | +| builderIndex | Widget Function(BuildContext context, String index, bool isActive)? | - | 索引文本自定义构建,包括索引激活左侧提示 | + +``` +``` + ### TDIndexesAnchor +#### 简介 +索引锚点 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| key | | - | | +| sticky | bool | - | 索引是否吸顶 | +| text | String | - | 锚点文本 | +| capsuleTheme | bool | - | 是否为胶囊式样式 | +| builderAnchor | Widget? Function(BuildContext context, String index, bool isPinnedToTop)? | - | 索引锚点构建 | +| activeIndex | ValueNotifier | - | 选中索引 | diff --git a/tdesign-component/example/assets/api/notice-bar_api.md b/tdesign-component/example/assets/api/notice-bar_api.md new file mode 100644 index 000000000..e54c1b6f9 --- /dev/null +++ b/tdesign-component/example/assets/api/notice-bar_api.md @@ -0,0 +1,34 @@ +## API +### TDNoticeBar +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| key | | - | | +| context | dynamic | - | 文本内容 | +| style | TDNoticeBarStyle? | - | 公告栏样式 | +| left | Widget? | - | 左侧内容(自定义左侧内容,优先级高于prefixIcon) | +| right | Widget? | - | 侧内容(自定义右侧内容,优先级高于suffixIcon) | +| marquee | bool? | false | 跑马灯效果 | +| speed | double? | 50 | 滚动速度 | +| interval | int? | 3000 | 步进滚动间隔时间(毫秒) | +| direction | Axis? | Axis.horizontal | 滚动方向 | +| theme | TDNoticeBarThemez? | TDNoticeBarThemez.info | 主题 | +| prefixIcon | IconData? | - | 左侧图标 | +| suffixIcon | IconData? | - | 右侧图标 | +| onTap | ValueChanged? | - | 点击事件 | + +``` +``` +### TDNoticeBarStyle +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext? | - | 上下文 | +| backgroundColor | Color? | - | 公告栏的背景色 | +| leftIconColor | Color? | - | 公告栏左侧图标的颜色 | +| rightIconColor | Color? | - | 公告栏右侧图标的颜色 | +| padding | EdgeInsetsGeometry? | EdgeInsets.only(top: 13, bottom: 13, left: 16, right: 12) | 公告栏的内边距 | +| textStyle | TextStyle? | TextStyle(color: TDTheme.of(context).fontGyColor1, fontSize: 16, height: 1) | 公告栏内容的文本样式 | + diff --git a/tdesign-component/example/assets/api/search_api.md b/tdesign-component/example/assets/api/search_api.md index 46908d44c..ef431c51f 100644 --- a/tdesign-component/example/assets/api/search_api.md +++ b/tdesign-component/example/assets/api/search_api.md @@ -20,3 +20,4 @@ | onActionClick | TDSearchBarEvent? | - | 右侧操作按钮点击回调 | | controller | TextEditingController? | - | 控制器 | | backgroundColor | Color? | Colors.white | 背景颜色 | + diff --git a/tdesign-component/example/assets/api/count-down_api.md b/tdesign-component/example/assets/api/time-counter_api.md similarity index 72% rename from tdesign-component/example/assets/api/count-down_api.md rename to tdesign-component/example/assets/api/time-counter_api.md index f5f269056..e11954222 100644 --- a/tdesign-component/example/assets/api/count-down_api.md +++ b/tdesign-component/example/assets/api/time-counter_api.md @@ -1,5 +1,5 @@ ## API -### TDCountDown +### TDTimeCounter #### 简介 倒计时组件 #### 默认构造方法 @@ -9,20 +9,21 @@ | key | | - | | | autoStart | bool | true | 是否自动开始倒计时 | | content | dynamic | 'default' | 'default' / Widget Function(int time) / Widget | -| format | String | 'HH:mm:ss' | 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒 | +| format | String | 'HH:mm:ss' | 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒(分隔符必须为长度为1的非空格的字符) | | millisecond | bool | false | 是否开启毫秒级渲染 | -| size | TDCountDownSize | TDCountDownSize.medium | 倒计时尺寸 | +| size | TDTimeCounterSize | TDTimeCounterSize.medium | 倒计时尺寸 | | splitWithUnit | bool | false | 使用时间单位分割 | -| theme | TDCountDownTheme | TDCountDownTheme.defaultTheme | 倒计时风格 | +| theme | TDTimeCounterTheme | TDTimeCounterTheme.defaultTheme | 倒计时风格 | | time | int | - | 必需;倒计时时长,单位毫秒 | -| style | TDCountDownStyle? | - | 自定义样式,有则优先用它,没有则根据size和theme选取 | +| style | TDTimeCounterStyle? | - | 自定义样式,有则优先用它,没有则根据size和theme选取 | | onChange | Function(int time)? | - | 时间变化时触发回调 | | onFinish | VoidCallback? | - | 倒计时结束时触发回调 | -| controller | TDCountDownController? | - | 控制器,可控制开始/暂停/继续/重置 | +| direction | TDTimeCounterDirection | TDTimeCounterDirection.down | 计时方向,默认倒计时 | +| controller | TDTimeCounterController? | - | 控制器,可控制开始/暂停/继续/重置 | ``` ``` - ### TDCountDownStyle + ### TDTimeCounterStyle #### 简介 倒计时组件样式 #### 默认构造方法 @@ -50,10 +51,10 @@ | 名称 | 说明 | | --- | --- | -| TDCountDownStyle.generateStyle | 生成默认样式 | +| TDTimeCounterStyle.generateStyle | 生成默认样式 | ``` ``` - ### TDCountDownController + ### TDTimeCounterController #### 简介 倒计时组件控制器,可控制开始(`start()`)/暂停(`pause()`)/继续(`resume()`)/重置(`reset([int? time])`) \ No newline at end of file diff --git a/tdesign-component/example/assets/code/search._buildFocusSearchBarWithAction.txt b/tdesign-component/example/assets/code/search._buildFocusSearchBarWithAction.txt new file mode 100644 index 000000000..95050bcea --- /dev/null +++ b/tdesign-component/example/assets/code/search._buildFocusSearchBarWithAction.txt @@ -0,0 +1,22 @@ + + Widget _buildFocusSearchBarWithAction(BuildContext context) { + return TDSearchBar( + placeHolder: '搜索预设文案', + action: '搜索', + needCancel: true, + controller: inputController, + onActionClick: () { + showGeneralDialog( + context: context, + pageBuilder: (BuildContext buildContext, Animation animation, + Animation secondaryAnimation) { + return TDConfirmDialog( + content: inputController.text.isNotEmpty + ? '搜索关键词:${inputController.text}' + : '搜索关键词为空', + ); + }, + ); + }, + ); + } \ No newline at end of file diff --git a/tdesign-component/example/lib/base/intl_resource_delegate.dart b/tdesign-component/example/lib/base/intl_resource_delegate.dart index 0b5eb9c98..b6e216db0 100644 --- a/tdesign-component/example/lib/base/intl_resource_delegate.dart +++ b/tdesign-component/example/lib/base/intl_resource_delegate.dart @@ -9,7 +9,7 @@ class IntlResourceDelegate extends TDResourceDelegate { BuildContext context; /// 国际化需要每次更新context - updateContext(BuildContext context){ + updateContext(BuildContext context) { this.context = context; } @@ -48,20 +48,88 @@ class IntlResourceDelegate extends TDResourceDelegate { @override String get reset => AppLocalizations.of(context)!.reset; - + @override String get days => AppLocalizations.of(context)!.days; - + @override String get hours => AppLocalizations.of(context)!.hours; - + @override String get milliseconds => AppLocalizations.of(context)!.milliseconds; - + @override String get minutes => AppLocalizations.of(context)!.minutes; - + @override String get seconds => AppLocalizations.of(context)!.seconds; -} \ No newline at end of file + @override + String get friday => AppLocalizations.of(context)!.friday; + + @override + String get monday => AppLocalizations.of(context)!.monday; + + @override + String get saturday => AppLocalizations.of(context)!.saturday; + + @override + String get sunday => AppLocalizations.of(context)!.sunday; + + @override + String get thursday => AppLocalizations.of(context)!.thursday; + + @override + String get tuesday => AppLocalizations.of(context)!.tuesday; + + @override + String get wednesday => AppLocalizations.of(context)!.wednesday; + + @override + String get year => AppLocalizations.of(context)!.year; + + @override + String get january => AppLocalizations.of(context)!.january; + + @override + String get february => AppLocalizations.of(context)!.february; + + @override + String get march => AppLocalizations.of(context)!.march; + + @override + String get april => AppLocalizations.of(context)!.april; + + @override + String get may => AppLocalizations.of(context)!.may; + + @override + String get june => AppLocalizations.of(context)!.june; + + @override + String get july => AppLocalizations.of(context)!.july; + + @override + String get august => AppLocalizations.of(context)!.august; + + @override + String get september => AppLocalizations.of(context)!.september; + + @override + String get october => AppLocalizations.of(context)!.october; + + @override + String get november => AppLocalizations.of(context)!.november; + + @override + String get december => AppLocalizations.of(context)!.december; + + @override + String get time => AppLocalizations.of(context)!.time; + + @override + String get start => AppLocalizations.of(context)!.start; + + @override + String get end => AppLocalizations.of(context)!.end; +} diff --git a/tdesign-component/example/lib/config.dart b/tdesign-component/example/lib/config.dart index fa515e92c..5a043f6d6 100644 --- a/tdesign-component/example/lib/config.dart +++ b/tdesign-component/example/lib/config.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'base/example_base.dart'; import 'page/sidebar/td_sidebar_page.dart'; import 'page/sidebar/td_sidebar_page_anchor.dart'; import 'page/sidebar/td_sidebar_page_custom.dart'; import 'page/sidebar/td_sidebar_page_icon.dart'; +import 'page/sidebar/td_sidebar_page_loading.dart'; import 'page/sidebar/td_sidebar_page_outline.dart'; import 'page/sidebar/td_sidebar_page_pagination.dart'; import 'page/td_avatar_page.dart'; @@ -13,10 +13,11 @@ import 'page/td_backtop_page.dart'; import 'page/td_badge_page.dart'; import 'page/td_bottom_tab_bar_page.dart'; import 'page/td_button_page.dart'; +import 'page/td_calendar_page.dart'; +import 'page/td_cascader_page.dart'; import 'page/td_cell_page.dart'; import 'page/td_checkbox_page.dart'; import 'page/td_collapse.dart'; -import 'page/td_count_down_page.dart'; import 'page/td_date_picker_page.dart'; import 'page/td_dialog_page.dart'; import 'page/td_divider_page.dart'; @@ -28,10 +29,12 @@ import 'page/td_font_page.dart'; import 'page/td_icon_page.dart'; import 'page/td_image_page.dart'; import 'page/td_image_viewer_page.dart'; +import 'page/td_indexes_page.dart'; import 'page/td_input_page.dart'; import 'page/td_link_page.dart'; import 'page/td_loading_page.dart'; import 'page/td_navbar_page.dart'; +import 'page/td_notice_bar_page.dart'; import 'page/td_picker_page.dart'; import 'page/td_popup_page.dart'; import 'page/td_radio_page.dart'; @@ -42,8 +45,8 @@ import 'page/td_search_bar_page.dart'; import 'page/td_shadows_page.dart'; import 'page/td_slider_page.dart'; import 'page/td_stepper_page.dart'; -import 'page/td_swipe_cell_page.dart'; import 'page/td_steps_page.dart'; +import 'page/td_swipe_cell_page.dart'; import 'page/td_swiper_page.dart'; import 'page/td_switch_page.dart'; import 'page/td_tabs_page.dart'; @@ -51,9 +54,9 @@ import 'page/td_tag_page.dart'; import 'page/td_text_page.dart'; import 'page/td_textarea_page.dart'; import 'page/td_theme_page.dart'; +import 'page/td_time_counter_page.dart'; import 'page/td_toast_page.dart'; import 'page/td_tree_select_page.dart'; -import 'page/td_cascader_page.dart'; import 'page/todo_page.dart'; PageBuilder _wrapInheritedTheme(WidgetBuilder builder) { @@ -91,8 +94,7 @@ Map> exampleMap = { ExamplePageModel( text: 'Indexes 索引', name: 'indexes', - isTodo: true, - pageBuilder: _wrapInheritedTheme((context) => const TodoPage())), + pageBuilder: _wrapInheritedTheme((context) => const TDIndexesPage())), ExamplePageModel( text: 'NavBar 导航栏', name: 'navbar', pageBuilder: _wrapInheritedTheme((context) => const TDNavBarPage())), ExamplePageModel( @@ -107,8 +109,7 @@ Map> exampleMap = { ExamplePageModel( text: 'Calendar 日历', name: 'calendar', - isTodo: true, - pageBuilder: _wrapInheritedTheme((context) => const TodoPage())), + pageBuilder: _wrapInheritedTheme((context) => const TDCalendarPage())), ExamplePageModel( text: 'Cascader 级联选择器', name: 'cascader', @@ -159,10 +160,9 @@ Map> exampleMap = { ExamplePageModel( text: 'Cell 单元格', name: 'cell', pageBuilder: _wrapInheritedTheme((context) => const TDCellPage())), ExamplePageModel( - text: 'CountDown 倒计时', - name: 'count-down', - pageName: 'count_down', - pageBuilder: _wrapInheritedTheme((context) => const TDCountDownPage())), + text: 'TimeCounter 计时器', + name: 'time-counter', + pageBuilder: _wrapInheritedTheme((context) => const TDTimeCounterPage())), ExamplePageModel( text: 'Collapse 折叠面板', name: 'collapse', @@ -227,10 +227,7 @@ Map> exampleMap = { isTodo: true, pageBuilder: _wrapInheritedTheme((context) => const TodoPage())), ExamplePageModel( - text: 'NoticeBar 公告栏', - name: 'notice_bar', - isTodo: true, - pageBuilder: _wrapInheritedTheme((context) => const TodoPage())), + text: 'NoticeBar 公告栏', name: 'notice-bar', pageBuilder: _wrapInheritedTheme((context) => const TDNoticeBarPage())), ExamplePageModel( text: 'Overlay 遮罩层', name: 'overlay', @@ -285,5 +282,10 @@ List sideBarExamplePage = [ text: 'SideBar 自定义样式', name: 'SideBarCustom', isTodo: false, - pageBuilder: _wrapInheritedTheme((context) => const TDSideBarCustomPage())) + pageBuilder: _wrapInheritedTheme((context) => const TDSideBarCustomPage())), + ExamplePageModel( + text: 'SideBar 延迟加载', + name: 'SideBarLoading', + isTodo: false, + pageBuilder: _wrapInheritedTheme((context) => const TDSideBarLoadingPage())) ]; diff --git a/tdesign-component/example/lib/l10n/app_en.arb b/tdesign-component/example/lib/l10n/app_en.arb index 07cc5f4a4..aa60c8588 100644 --- a/tdesign-component/example/lib/l10n/app_en.arb +++ b/tdesign-component/example/lib/l10n/app_en.arb @@ -19,5 +19,28 @@ "hours": "hours", "minutes": "minutes", "seconds": "seconds", - "milliseconds": "milliseconds" + "milliseconds": "milliseconds", + "sunday": "SUN", + "monday": "MON", + "tuesday": "TUE", + "wednesday": "WED", + "thursday": "THU", + "friday": "FRI", + "saturday": "SAT", + "year": "", + "january": "January", + "february": "February", + "march": "March", + "april": "April", + "may": "May", + "june": "June", + "july": "July", + "august": "August", + "september": "September", + "october": "October", + "november": "November", + "december": "December", + "time": "Time", + "start": "Start", + "end": "End" } \ No newline at end of file diff --git a/tdesign-component/example/lib/l10n/app_zh.arb b/tdesign-component/example/lib/l10n/app_zh.arb index 95eb6d55a..9400960a3 100644 --- a/tdesign-component/example/lib/l10n/app_zh.arb +++ b/tdesign-component/example/lib/l10n/app_zh.arb @@ -19,5 +19,28 @@ "hours": "时", "minutes": "分", "seconds": "秒", - "milliseconds": "毫秒" + "milliseconds": "毫秒", + "sunday": "日", + "monday": "一", + "tuesday": "二", + "wednesday": "三", + "thursday": "四", + "friday": "五", + "saturday": "六", + "year": " 年", + "january": "1 月", + "february": "2 月", + "march": "3 月", + "april": "4 月", + "may": "5 月", + "june": "6 月", + "july": "7 月", + "august": "8 月", + "september": "9 月", + "october": "10 月", + "november": "11 月", + "december": "12 月", + "time": "时间", + "start": "开始", + "end": "结束" } \ No newline at end of file diff --git a/tdesign-component/example/lib/page/sidebar/td_sidebar_page.dart b/tdesign-component/example/lib/page/sidebar/td_sidebar_page.dart index ce5e48915..743cec47f 100644 --- a/tdesign-component/example/lib/page/sidebar/td_sidebar_page.dart +++ b/tdesign-component/example/lib/page/sidebar/td_sidebar_page.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; @@ -43,7 +44,12 @@ class TDSideBarPageState extends State { ExampleItem( desc: '侧边导航样式', ignoreCode: true, builder: _buildStyleSideBar), ]) - ]); + ], + test: [ + ExampleItem( + desc: '延迟加载', ignoreCode: true, builder: _loadingSideBar), + ], + ); } Widget _buildNavigatorSideBar(BuildContext context) { @@ -99,6 +105,20 @@ class TDSideBarPageState extends State { )); } + Widget _loadingSideBar(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + CodeWrapper( + builder: (_) => + getCustomButton(context, '延迟加载', 'SideBarLoading'), + methodName: '_buildLoadingSideBar', + ), + ], + )); + } + TDButton getCustomButton( BuildContext context, String text, String routeName) { return TDButton( diff --git a/tdesign-component/example/lib/page/sidebar/td_sidebar_page_loading.dart b/tdesign-component/example/lib/page/sidebar/td_sidebar_page_loading.dart new file mode 100644 index 000000000..29d1a2032 --- /dev/null +++ b/tdesign-component/example/lib/page/sidebar/td_sidebar_page_loading.dart @@ -0,0 +1,217 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +import '../../annotation/demo.dart'; +import '../../base/example_widget.dart'; + +/// +/// TDSideBarLoadingPage演示 +/// +class TDSideBarLoadingPage extends StatefulWidget { + const TDSideBarLoadingPage({Key? key}) : super(key: key); + + @override + State createState() { + return TDSideBarLoadingPageState(); + } +} + +class TDSideBarLoadingPageState extends State { + var currentValue = 1; + var itemHeight = 278.5; + final _demoScroller = ScrollController(initialScrollOffset: 278.5); + final _sideBarController = TDSideBarController(); + static const threshold = 50; + var lock = false; + + @override + void initState() { + super.initState(); + + _demoScroller.addListener(() { + if (lock) { + return; + } + + var scrollTop = _demoScroller.offset; + var index = (scrollTop + threshold) ~/ itemHeight; + + if (currentValue != index) { + setState(() { + _sideBarController.selectTo(index); + }); + } + }); + } + + Future onSelected(int value) async { + if (currentValue != value) { + setState(() { + currentValue = value; + }); + + lock = true; + await _demoScroller.animateTo(value.toDouble() * itemHeight, + duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + lock = false; + } + } + + void onChanged(int value) { + setState(() { + currentValue = value; + }); + } + + @override + Widget build(BuildContext context) { + var current = buildWidget(context); + return current; + } + + Widget buildWidget(BuildContext context) { + return ExamplePage( + title: 'SideBar 延迟加载', + exampleCodeGroup: 'sideBar', + showSingleChild: true, + singleChild: CodeWrapper( + isCenter: false, + builder: _buildLoadingSideBar, + )); + } + + final list = []; + final pages = []; + + void _initData() { + for (var i = 0; i < 20; i++) { + list.add(SideItemProps( + index: i, + label: '选项', + value: i, + )); + pages.add(getLoadingDemo(i)); + } + + pages.add(Container( + height: MediaQuery.of(context).size.height - itemHeight, + decoration: const BoxDecoration(color: Colors.white), + )); + + list[1].badge = const TDBadge(TDBadgeType.redPoint); + list[2].badge = const TDBadge( + TDBadgeType.message, + count: '8', + ); + if(_sideBarController.loading) { + _sideBarController.loading = false; + _sideBarController.init(list); + _sideBarController.selectTo(currentValue); + } + } + + @Demo(group: 'sideBar') + Widget _buildLoadingSideBar(BuildContext context) { + // 延迟加载 + Future.delayed(const Duration(seconds: 3), _initData); + var size = MediaQuery.of(context).size; + var demoHeight = size.height; + + return Row( + children: [ + SizedBox( + width: list.isEmpty ? size.width : 110, + child: TDSideBar( + height: demoHeight, + style: TDSideBarStyle.normal, + value: currentValue, + controller: _sideBarController, + loading: true, + children: list + .map((ele) => TDSideBarItem( + label: ele.label ?? '', + badge: ele.badge, + value: ele.value, + icon: ele.icon)) + .toList(), + onChanged: onChanged, + onSelected: onSelected, + ), + ), + Expanded( + child: SizedBox( + height: demoHeight, + child: SingleChildScrollView( + controller: _demoScroller, + child: Column( + children: pages, + ), + ), + )) + ], + ); + } + + Widget getLoadingDemo(int index) { + return Container( + decoration: const BoxDecoration(color: Colors.white), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 20, top: 15, right: 9), + child: TDText('标题$index', + style: const TextStyle( + fontSize: 14, + )), + ), + Padding( + padding: const EdgeInsets.only(left: 20), + child: displayImageList(), + ), + ], + ), + ); + } + + Widget displayImageList() { + return Column( + children: [ + displayImageItem(), + const TDDivider(), + displayImageItem(), + const TDDivider(), + displayImageItem(), + const TDDivider(), + ], + ); + } + + Widget displayImageItem() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + TDImage( + assetUrl: 'assets/img/empty.png', + type: TDImageType.roundedSquare, + width: 48, + height: 48, + ), + SizedBox( + width: 16, + ), + TDText( + '标题', + style: TextStyle( + fontSize: 16, + ), + ) + ], + ), + ); + } +} diff --git a/tdesign-component/example/lib/page/td_calendar_page.dart b/tdesign-component/example/lib/page/td_calendar_page.dart new file mode 100644 index 000000000..67ec286bf --- /dev/null +++ b/tdesign-component/example/lib/page/td_calendar_page.dart @@ -0,0 +1,318 @@ +import 'package:flutter/material.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; +import '../../base/example_widget.dart'; +import '../annotation/demo.dart'; + +class TDCalendarPage extends StatelessWidget { + const TDCalendarPage({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + color: TDTheme.of(context).grayColor2, + child: ExamplePage( + title: tdTitle(context), + desc: '按照日历形式展示数据或日期的容器。', + exampleCodeGroup: 'calendar', + children: [ + ExampleModule(title: '组件类型', children: [ + ExampleItem( + ignoreCode: true, + center: false, + builder: (BuildContext context) { + return const CodeWrapper(builder: _buildSimple); + }, + ), + ]), + ExampleModule(title: '组件样式', children: [ + ExampleItem( + desc: '可以自由定义想要的风格', + ignoreCode: true, + center: false, + builder: (BuildContext context) { + return const CodeWrapper(builder: _buildStyle); + }, + ), + ExampleItem( + desc: '不使用Popup', + ignoreCode: true, + center: false, + builder: (BuildContext context) { + return const CodeWrapper(builder: _buildBlock); + }, + ), + ]), + ], + test: [], + ), + ); + } +} + +@Demo(group: 'calendar') +Widget _buildSimple(BuildContext context) { + final size = MediaQuery.of(context).size; + final selected = ValueNotifier>([DateTime.now().millisecondsSinceEpoch]); + return ValueListenableBuilder( + valueListenable: selected, + builder: (context, value, child) { + final date = DateTime.fromMillisecondsSinceEpoch(value[0]); + return TDCellGroup( + cells: [ + TDCell( + title: '单个选择日历', + arrow: true, + note: '${date.year}-${date.month}-${date.day}', + onClick: (cell) { + TDCalendarPopup( + context, + visible: true, + onConfirm: (value) { + print('onConfirm:$value'); + selected.value = value; + }, + onClose: () { + print('onClose'); + }, + child: TDCalendar( + title: '请选择日期', + value: value, + height: size.height * 0.6 + 176, + onCellClick: (value, type, tdate) { + print('onCellClick:$value'); + }, + onCellLongPress: (value, type, tdate) { + print('onCellLongPress:$value'); + }, + onHeanderClick: (index, week) { + print('onHeanderClick:$week'); + }, + onChange: (value) { + print('onChange:$value'); + }, + ), + ); + }, + ), + TDCell( + title: '多个选择日历', + arrow: true, + onClick: (cell) { + TDCalendarPopup( + context, + visible: true, + child: TDCalendar( + title: '请选择日期', + type: CalendarType.multiple, + value: [DateTime.now().millisecondsSinceEpoch], + height: size.height * 0.6 + 176, + ), + ); + }, + ), + TDCell( + title: '区间选择日历', + arrow: true, + onClick: (cell) { + TDCalendarPopup( + context, + visible: true, + child: TDCalendar( + title: '请选择日期区间', + type: CalendarType.range, + value: [ + DateTime.now().millisecondsSinceEpoch, + DateTime.now().add(const Duration(days: 6)).millisecondsSinceEpoch, + ], + height: size.height * 0.6 + 176, + ), + ); + }, + ), + TDCell( + title: '单个选择日历和时间', + arrow: true, + note: '${date.year}-${date.month}-${date.day} ${date.hour}:${date.minute}', + onClick: (cell) { + TDCalendarPopup( + context, + visible: true, + onConfirm: (value) { + print('onConfirm:$value'); + selected.value = value; + }, + onClose: () { + print('onClose'); + }, + child: TDCalendar( + title: '请选择日期和时间', + value: value, + height: size.height * 0.92, + useTimePicker: true, + onCellClick: (value, type, tdate) { + print('onCellClick:$value'); + }, + onCellLongPress: (value, type, tdate) { + print('onCellLongPress:$value'); + }, + onHeanderClick: (index, week) { + print('onHeanderClick:$week'); + }, + onChange: (value) { + print('onChange:$value'); + }, + ), + ); + }, + ), + TDCell( + title: '区间选择日历和时间', + arrow: true, + onClick: (cell) { + TDCalendarPopup( + context, + visible: true, + onConfirm: (value) { + print('onConfirm:$value'); + }, + onClose: () { + print('onClose'); + }, + child: TDCalendar( + title: '请选择日期和时间区间', + height: size.height * 0.92, + type: CalendarType.range, + value: [ + DateTime.now().millisecondsSinceEpoch, + DateTime.now().add(const Duration(days: 3)).millisecondsSinceEpoch, + ], + useTimePicker: true, + onCellClick: (value, type, tdate) { + print('onCellClick:$value'); + }, + onCellLongPress: (value, type, tdate) { + print('onCellLongPress:$value'); + }, + onHeanderClick: (index, week) { + print('onHeanderClick:$week'); + }, + onChange: (value) { + print('onChange:$value'); + }, + ), + ); + }, + ), + ], + ); + }, + ); +} + +@Demo(group: 'calendar') +Widget _buildStyle(BuildContext context) { + final size = MediaQuery.of(context).size; + const map = { + 1: '初一', + 2: '初二', + 3: '初三', + 14: '情人节', + 15: '元宵节', + }; + return TDCellGroup( + cells: [ + TDCell( + title: '自定义文案', + arrow: true, + onClick: (cell) { + TDCalendarPopup( + context, + visible: true, + child: TDCalendar( + title: '请选择日期', + height: size.height * 0.6 + 176, + minDate: DateTime(2022, 1, 1).millisecondsSinceEpoch, + maxDate: DateTime(2022, 2, 15).millisecondsSinceEpoch, + format: (day) { + day?.suffix = '¥60'; + if (day?.date.month == 2) { + if (map.keys.contains(day?.date.day)) { + day?.suffix = '¥100'; + day?.prefix = map[day.date.day]; + day?.style = TextStyle( + fontSize: TDTheme.of(context).fontTitleMedium?.size, + height: TDTheme.of(context).fontTitleMedium?.height, + fontWeight: TDTheme.of(context).fontTitleMedium?.fontWeight, + color: TDTheme.of(context).errorColor6, + ); + if (day?.typeNotifier.value == DateSelectType.selected) { + day?.style = day.style?.copyWith(color: TDTheme.of(context).fontWhColor1); + } + } + } + return null; + }, + ), + ); + }, + ), + TDCell( + title: '自定义按钮', + arrow: true, + onClick: (cell) { + late final TDCalendarPopup calendar; + calendar = TDCalendarPopup( + context, + visible: true, + confirmBtn: Padding( + padding: EdgeInsets.symmetric(vertical: TDTheme.of(context).spacer16), + child: TDButton( + theme: TDButtonTheme.danger, + shape: TDButtonShape.round, + text: 'ok', + isBlock: true, + size: TDButtonSize.large, + onTap: () { + print(calendar.selected); + calendar.close(); + }, + ), + ), + child: TDCalendar( + title: '请选择日期', + value: [DateTime.now().millisecondsSinceEpoch], + height: size.height * 0.6 + 176, + ), + ); + }, + ), + TDCell( + title: '自定义日期区间', + arrow: true, + onClick: (cell) { + TDCalendarPopup( + context, + visible: true, + child: TDCalendar( + title: '请选择日期', + minDate: DateTime(2022, 1, 1).millisecondsSinceEpoch, + maxDate: DateTime(2022, 1, 31).millisecondsSinceEpoch, + value: [DateTime(2022, 1, 15).millisecondsSinceEpoch], + height: size.height * 0.6 + 176, + ), + ); + }, + ), + ], + ); +} + +@Demo(group: 'calendar') +Widget _buildBlock(BuildContext context) { + final size = MediaQuery.of(context).size; + return TDCalendar( + title: '请选择日期', + value: [DateTime.now().millisecondsSinceEpoch], + height: size.height * 0.6 + 176, + ); +} diff --git a/tdesign-component/example/lib/page/td_cascader_page.dart b/tdesign-component/example/lib/page/td_cascader_page.dart index 2ea5b6c7a..f122151f0 100644 --- a/tdesign-component/example/lib/page/td_cascader_page.dart +++ b/tdesign-component/example/lib/page/td_cascader_page.dart @@ -1,4 +1,7 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../annotation/demo.dart'; import '../../base/example_widget.dart'; @@ -144,14 +147,16 @@ class _TDCascaderPageState extends State { String? _initData_3; String _selected_3 = ''; - List _data_3 = [ + final List _data_3 = [ { "label": '技术部门', "value": '110000', + "segmentValue": 'J', "children": [ { "value": '110100', "label": '部门一', + "segmentValue": 'B', "children": [ {"value": '110101', "label": '洪磊', "segmentValue": 'H'}, {"value": '110102', "label": '洪磊2', "segmentValue": 'H'}, @@ -159,22 +164,23 @@ class _TDCascaderPageState extends State { {"value": '110105', "label": '洪磊4', "segmentValue": 'H'}, {"value": '110106', "label": '郭天1', "segmentValue": 'G'}, {"value": '110107', "label": '郭天2', "segmentValue": 'G'}, - {"value": '110108', "label": '郭天3', "segmentValue": 'G'}, {"value": '110109', "label": '冯笑1', "segmentValue": 'F'}, + {"value": '110108', "label": '郭天3', "segmentValue": 'G'}, ], }, { "value": '110200', "label": '部门二', + "segmentValue": 'B', "children": [ - {"value": '110201', "label": '张雷1'}, - {"value": '110202', "label": '张雷2'}, - {"value": '1102022', "label": '张雷3'}, - {"value": '110205', "label": '张雷4'}, - {"value": '110206', "label": '张雷5'}, - {"value": '110207', "label": '张雷6'}, - {"value": '110208', "label": '张雷7'}, - {"value": '110209', "label": '张雷8'}, + {"value": '110201', "label": '洪磊', "segmentValue": 'H'}, + {"value": '110205', "label": '洪磊4', "segmentValue": 'H'}, + {"value": '110206', "label": '郭天1', "segmentValue": 'G'}, + {"value": '110207', "label": '郭天2', "segmentValue": 'G'}, + {"value": '110208', "label": '郭天3', "segmentValue": 'G'}, + {"value": '110209', "label": '冯笑1', "segmentValue": 'F'}, + {"value": '110202', "label": '洪磊2', "segmentValue": 'H'}, + {"value": '1102022', "label": '洪磊3', "segmentValue": 'H'}, ], }, ], @@ -182,26 +188,91 @@ class _TDCascaderPageState extends State { { "label": '行政部门', "value": '120000', + "segmentValue": 'X', "children": [ { "value": '120100', "label": '部门一', + "segmentValue": 'B', + "children": [ + {"value": '120201', "label": '洪磊', "segmentValue": 'H'}, + {"value": '120205', "label": '洪磊4', "segmentValue": 'H'}, + {"value": '120206', "label": '郭天1', "segmentValue": 'G'}, + {"value": '120207', "label": '郭天2', "segmentValue": 'G'}, + {"value": '120208', "label": '郭天3', "segmentValue": 'G'}, + {"value": '120209', "label": '冯笑1', "segmentValue": 'F'}, + {"value": '120202', "label": '洪磊2', "segmentValue": 'H'}, + {"value": '1202022', "label": '洪磊3', "segmentValue": 'H'}, + ], + }, + ], + }, + ]; + + String? _initData_4; + String _selected_4 = ''; + final List _data_4 = [ + { + "label": '技术部门', + "value": '110000', + "children": [ + { + "value": '110100', + "label": '部门一', + "children": [ + {"value": '110201', "label": '后勤部门', "children":[ + { + "value": '110301', "label": '后勤A组',"children":[ + { + "value": '110401', "label": '一组',"children":[ + {"value": '110501', "label": '洪磊',}, + {"value": '110502', "label": '洪磊2'}, + {"value": '110506', "label": '郭天1'}, + {"value": '110507', "label": '郭天2'}, + {"value": '110508', "label": '郭天3'}, + {"value": '110509', "label": '冯笑1'}, + {"value": '1105022', "label": '洪磊3'}, + {"value": '110505', "label": '洪磊4'}, + ] + } + ] + } + ]}, + ], + }, + { + "value": '120100', + "label": '部门二', "children": [ - {"value": '120101', "label": '张雷1'}, - {"value": '120102', "label": '张雷2'}, - {"value": '120103', "label": '张雷3'}, - {"value": '120104', "label": '张雷4'}, - {"value": '120105', "label": '张雷5'}, - {"value": '120106', "label": '张雷6'}, - {"value": '120110', "label": '张雷7'}, - {"value": '120111', "label": '张雷8'}, - {"value": '120112', "label": '张雷9'}, + {"value": '120201', "label": '后勤部门', "children":[ + { + "value": '120301', "label": '后勤A组',"children":[ + { + "value": '120401', "label": '一组',"children":[ + {"value": '120501', "label": '张雷1'}, + {"value": '120502', "label": '张雷2'}, + {"value": '1205022', "label": '张雷3'}, + {"value": '120505', "label": '张雷4'}, + {"value": '120506', "label": '张雷5'}, + {"value": '120507', "label": '张雷6'}, + {"value": '120508', "label": '张雷7'}, + {"value": '120509', "label": '张雷8'}, + ] + } + ] + } + ]}, ], }, ], }, ]; @override + void initState() { + // TODO: implement initState + super.initState(); + } + @override Widget build(BuildContext context) { return Container( color: TDTheme.of(context).whiteColor1, @@ -211,18 +282,17 @@ class _TDCascaderPageState extends State { desc: '用于多层级数据的逐级选择', children: [ ExampleModule(title: '组件类型', children: [ - ExampleItem(desc: '垂直级联选择器', builder: _buildVerticalCascader), - ExampleItem(desc: '垂直级联选择器-带字母定位', builder: _buildVerticalLetterCascader), + ExampleItem(desc: '垂直级联选择器', builder: _buildVerticalCascader), + ExampleItem(desc: '垂直级联选择器-带字母定位', builder: _buildVerticalLetterCascader), ExampleItem(desc: '水平级联选择器', builder: _buildHorizontalCascader), - ExampleItem(desc: '水平级联选择器-带字母定位', builder: _buildHorizontalLetterCascader), + ExampleItem(desc: '水平级联选择器-带字母定位', builder: _buildHorizontalLetterCascader), ExampleItem(desc: '水平级联选择器-部门', builder: _buildHorizontalCompanyCascader), ExampleItem(desc: '垂直级联选择器-部门', builder: _buildVerticalCompanyCascader), ]), ], test: [ - ExampleItem( - desc: '测试使用次标题', - builder: _buildVerticalSubTitleCascader), + ExampleItem(desc: '测试使用次标题', builder: _buildVerticalSubTitleCascader), + ExampleItem(desc: '垂直级联选择器-部门', builder: _buildTestVerticalCompanyCascader), ], ), ); @@ -305,8 +375,12 @@ class _TDCascaderPageState extends State { Widget _buildHorizontalLetterCascader(BuildContext context) { return GestureDetector( onTap: () { - TDCascader.showMultiCascader(context, title: '选择地址', data: _data_2, initialData: _initData_2, theme: 'tab', - onChange: (List selectData) { + TDCascader.showMultiCascader(context, + title: '选择地址', + data: _data_2, + initialData: _initData_2, + isLetterSort: true, + theme: 'tab', onChange: (List selectData) { setState(() { List result = []; int len = selectData.length; @@ -328,7 +402,7 @@ class _TDCascaderPageState extends State { Widget _buildHorizontalCompanyCascader(BuildContext context) { return GestureDetector( onTap: () { - TDCascader.showMultiCascader(context, title: '选择部门人员', data: _data_3, initialData: _initData_3, theme: 'tab', + TDCascader.showMultiCascader(context, title: '选择部门人员', data: _data_3,isLetterSort: true, initialData: _initData_3, theme: 'tab', onChange: (List selectData) { setState(() { List result = []; @@ -351,7 +425,7 @@ class _TDCascaderPageState extends State { Widget _buildVerticalCompanyCascader(BuildContext context) { return GestureDetector( onTap: () { - TDCascader.showMultiCascader(context, title: '选择部门人员', data: _data_3, initialData: _initData_3, theme: 'step', + TDCascader.showMultiCascader(context, title: '选择部门人员', data: _data_3,isLetterSort: true, initialData: _initData_3, theme: 'step', onChange: (List selectData) { setState(() { List result = []; @@ -396,7 +470,31 @@ class _TDCascaderPageState extends State { child: _buildSelectRow(context, _selected_1, '选择地区'), ); } - + @Demo(group: 'cascader') + Widget _buildTestVerticalCompanyCascader(BuildContext context) { + return GestureDetector( + onTap: () { + TDCascader.showMultiCascader(context, + title: '选择部门人员', + data: _data_4, + initialData: _initData, + theme: 'step', onChange: (List selectData) { + setState(() { + List result = []; + int len = selectData.length; + _initData = selectData[len - 1].value!; + selectData.forEach((element) { + result.add(element.label); + }); + _selected_4 = result.join('/'); + }); + }, onClose: () { + Navigator.of(context).pop(); + }); + }, + child: _buildSelectRow(context, _selected_4, '选择部门人员'), + ); + } Widget _buildSelectRow(BuildContext context, String output, String title) { return Container( color: TDTheme.of(context).whiteColor1, diff --git a/tdesign-component/example/lib/page/td_cell_page.dart b/tdesign-component/example/lib/page/td_cell_page.dart index 9ec21100d..b6c3ad52a 100644 --- a/tdesign-component/example/lib/page/td_cell_page.dart +++ b/tdesign-component/example/lib/page/td_cell_page.dart @@ -44,23 +44,15 @@ class TDCellPage extends StatelessWidget { ), ]), ], - test: const [ - // ExampleItem( - // ignoreCode: true, - // desc: '显示外边框', - // center: false, - // builder: (BuildContext context) { - // return const CodeWrapper(builder: _buildBorder); - // }, - // ), - // ExampleItem( - // ignoreCode: true, - // desc: '显示标题', - // center: false, - // builder: (BuildContext context) { - // return const CodeWrapper(builder: _buildTitle); - // }, - // ), + test: [ + ExampleItem( + ignoreCode: true, + desc: '自定义内边距-padding', + center: false, + builder: (BuildContext context) { + return const CodeWrapper(builder: _buildPadding); + }, + ), ], )); } @@ -124,6 +116,18 @@ Widget _buildCard(BuildContext context) { ); } +@Demo(group: 'cell') +Widget _buildPadding(BuildContext context) { + var style = TDCellStyle.cellStyle(context); + style.padding = const EdgeInsets.all(30); + return TDCellGroup( + theme: TDCellGroupTheme.cardTheme, + cells: [ + TDCell(arrow: true, title: 'padding-all-30', style: style,), + ], + ); +} + // @Demo(group: 'cell') // Widget _buildBorder(BuildContext context) { // return const TDCellGroup( @@ -150,4 +154,4 @@ Widget _buildCard(BuildContext context) { // TDCell(title: 'item', leftIcon: TDIcons.app), // ], // ); -// } \ No newline at end of file +// } diff --git a/tdesign-component/example/lib/page/td_dialog_page.dart b/tdesign-component/example/lib/page/td_dialog_page.dart index 336277d24..a3ee1267c 100644 --- a/tdesign-component/example/lib/page/td_dialog_page.dart +++ b/tdesign-component/example/lib/page/td_dialog_page.dart @@ -68,6 +68,7 @@ class _TDDialogPageState extends State { ExampleItem(builder: _customConfirmNormal), ExampleItem(builder: _customConfirmVertical), ExampleItem(builder: _customImageTop), + ExampleItem(desc: '自定义边距和按钮', builder: _customContentAndBtn) ],); } @@ -734,4 +735,37 @@ class _TDDialogPageState extends State { }, ); } + + @Demo(group: 'dialog') + Widget _customContentAndBtn(BuildContext context) { + return TDButton( + text: '自定义边距和按钮', + size: TDButtonSize.large, + type: TDButtonType.outline, + theme: TDButtonTheme.primary, + onTap: () { + showGeneralDialog( + context: context, + pageBuilder: (BuildContext buildContext, Animation animation, + Animation secondaryAnimation) { + return TDConfirmDialog( + title: _dialogTitle, + content: _commonContent, + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + buttonWidget: Container( + padding: const EdgeInsets.fromLTRB(0, 16, 0, 16), + child: TDButton( + text: '自定义按钮', + theme: TDButtonTheme.primary, + onTap: () { + Navigator.of(context).pop(); + }, + ), + ), + ); + } + ); + } + ); + } } diff --git a/tdesign-component/example/lib/page/td_drawer_page.dart b/tdesign-component/example/lib/page/td_drawer_page.dart index b96b88f0b..cec142f42 100644 --- a/tdesign-component/example/lib/page/td_drawer_page.dart +++ b/tdesign-component/example/lib/page/td_drawer_page.dart @@ -81,7 +81,15 @@ class TDDrawerPage extends StatelessWidget { ), ]), ], - test: const [], + test: [ + ExampleItem( + ignoreCode: true, + desc: '自定义背景色', + builder: (BuildContext context) { + return const CodeWrapper(builder: _buildColorSimple); + }, + ) + ], )); } } @@ -182,3 +190,26 @@ Widget _buildBottomSimple(BuildContext context) { }, ); } + +@Demo(group: 'drawer') +Widget _buildColorSimple(BuildContext context) { + var renderBox = navBarkey.currentContext?.findRenderObject() as RenderBox?; + return TDButton( + text: '自定义背景色', + isBlock: true, + type: TDButtonType.outline, + theme: TDButtonTheme.primary, + size: TDButtonSize.large, + onTap: () { + TDDrawer( + context, + visible: true, + drawerTop: renderBox?.size.height, + title: '标题', + backgroundColor: TDTheme.of(context).grayColor1, + placement: TDDrawerPlacement.right, + items: List.generate(10, (index) => TDDrawerItem(title: '菜单${_nums[index]}')).toList(), + ); + }, + ); +} diff --git a/tdesign-component/example/lib/page/td_dropdown_menu_page.dart b/tdesign-component/example/lib/page/td_dropdown_menu_page.dart index 5d3ce60ca..761970abe 100644 --- a/tdesign-component/example/lib/page/td_dropdown_menu_page.dart +++ b/tdesign-component/example/lib/page/td_dropdown_menu_page.dart @@ -70,6 +70,13 @@ class TDDropdownMenuPage extends StatelessWidget { return const CodeWrapper(builder: _buildHeight); }, ), + ExampleItem( + ignoreCode: true, + desc: '可横向滚动菜单', + builder: (BuildContext context) { + return const CodeWrapper(builder: _buildOverflow); + }, + ), ], )); } @@ -346,3 +353,75 @@ TDDropdownMenu _buildHeight(BuildContext context) { }, ); } + +@Demo(group: 'dropdownMenu') +TDDropdownMenu _buildOverflow(BuildContext context) { + return TDDropdownMenu( + isScrollable: true, + tabBarAlign: MainAxisAlignment.spaceAround, + direction: TDDropdownMenuDirection.up, + onMenuOpened: (value) { + print('打开第$value个菜单'); + }, + onMenuClosed: (value) { + print('关闭第$value个菜单'); + }, + builder: (context) { + return [ + TDDropdownItem( + label: '最大高度限制', + multiple: true, + maxHeight: 200, + tabBarWidth: 200, + options: [ + TDDropdownItemOption(label: '选项1', value: '1', selected: true), + TDDropdownItemOption(label: '选项2', value: '2', selected: true), + TDDropdownItemOption(label: '选项3', value: '3', selected: true), + TDDropdownItemOption(label: '选项4', value: '4'), + TDDropdownItemOption(label: '选项5', value: '5'), + TDDropdownItemOption(label: '选项6', value: '6'), + TDDropdownItemOption(label: '选项7', value: '7'), + TDDropdownItemOption(label: '选项8', value: '8'), + TDDropdownItemOption(label: '选项9', value: '9'), + TDDropdownItemOption(label: '禁用选项', value: '10', disabled: true), + TDDropdownItemOption(label: '禁用选项', value: '11', disabled: true), + TDDropdownItemOption(label: '禁用选项', value: '12', disabled: true), + ], + onChange: (value) { + print('选择:$value'); + }, + ), + TDDropdownItem( + maxHeight: 200, + tabBarWidth: 200, + tabBarAlign: MainAxisAlignment.start, + options: [ + TDDropdownItemOption(label: '选项1', value: '1', selected: true), + TDDropdownItemOption(label: '选项2', value: '2'), + ], + ), + TDDropdownItem( + maxHeight: 200, + options: [ + TDDropdownItemOption(label: '选项1', value: '1', selected: true), + TDDropdownItemOption(label: '选项2', value: '2'), + ], + ), + TDDropdownItem( + maxHeight: 200, + options: [ + TDDropdownItemOption(label: '选项1', value: '1', selected: true), + TDDropdownItemOption(label: '选项2', value: '2'), + ], + ), + TDDropdownItem( + maxHeight: 200, + options: [ + TDDropdownItemOption(label: '选项1', value: '1', selected: true), + TDDropdownItemOption(label: '选项2', value: '2'), + ], + ), + ]; + }, + ); +} \ No newline at end of file diff --git a/tdesign-component/example/lib/page/td_image_viewer_page.dart b/tdesign-component/example/lib/page/td_image_viewer_page.dart index 4a9fafd10..91f9b4e1e 100644 --- a/tdesign-component/example/lib/page/td_image_viewer_page.dart +++ b/tdesign-component/example/lib/page/td_image_viewer_page.dart @@ -54,6 +54,10 @@ class _TDImageViewerPageState extends State { @Demo(group: 'image_viewer') Widget _actionImageViewer(BuildContext context) { + var delImages = [ + 'https://tdesign.gtimg.com/mobile/demos/swiper1.png', + 'https://tdesign.gtimg.com/mobile/demos/swiper2.png', + ]; return TDButton( type: TDButtonType.ghost, theme: TDButtonTheme.primary, @@ -63,7 +67,7 @@ class _TDImageViewerPageState extends State { onTap: () { TDImageViewer.showImageViewer( context: context, - images: images, + images: delImages, showIndex: true, deleteBtn: true, ); diff --git a/tdesign-component/example/lib/page/td_indexes_page.dart b/tdesign-component/example/lib/page/td_indexes_page.dart new file mode 100644 index 000000000..e5b4afc56 --- /dev/null +++ b/tdesign-component/example/lib/page/td_indexes_page.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; +import '../../base/example_widget.dart'; +import '../annotation/demo.dart'; + +const _list = [ + { + 'index': 'A', + 'children': ['阿坝', '阿拉善', '阿里', '安康', '安庆', '鞍山', '安顺', '安阳', '澳门'], + }, + { + 'index': 'B', + 'children': [ + '北京', + '白银', + '保定', + '宝鸡', + '保山', + '包头', + '巴中', + '北海', + '蚌埠', + '本溪', + '毕节', + '滨州', + '百色', + '亳州', + ], + }, + { + 'index': 'C', + 'children': [ + '重庆', + '成都', + '长沙', + '长春', + '沧州', + '常德', + '昌都', + '长治', + '常州', + '巢湖', + '潮州', + '承德', + '郴州', + '赤峰', + '池州', + '崇左', + '楚雄', + '滁州', + '朝阳', + ], + }, + { + 'index': 'D', + 'children': [ + '大连', + '东莞', + '大理', + '丹东', + '大庆', + '大同', + '大兴安岭', + '德宏', + '德阳', + '德州', + '定西', + '迪庆', + '东营', + ], + }, + { + 'index': 'E', + 'children': ['鄂尔多斯', '恩施', '鄂州'], + }, + { + 'index': 'F', + 'children': ['福州', '防城港', '佛山', '抚顺', '抚州', '阜新', '阜阳'], + }, + { + 'index': 'G', + 'children': ['广州', '桂林', '贵阳', '甘南', '赣州', '甘孜', '广安', '广元', '贵港', '果洛'], + }, + { + 'index': 'J', + 'children': ['揭阳', '吉林', '晋江', '吉安', '胶州', '嘉兴', '济南', '鸡西', '荆州', '江门', '基隆'], + }, + { + 'index': 'K', + 'children': ['昆明', '开封', '康定', '喀什'], + }, +]; + +class TDIndexesPage extends StatelessWidget { + const TDIndexesPage({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + color: TDTheme.of(context).grayColor2, + child: ExamplePage( + title: tdTitle(context), + desc: '用于页面中信息快速检索,可以根据目录中的页码快速找到所需的内容。', + exampleCodeGroup: 'indexes', + children: [ + ExampleModule(title: '组件类型', children: [ + ExampleItem( + ignoreCode: true, + desc: '基础索引类型', + builder: (BuildContext context) { + return const CodeWrapper(builder: _buildSimple); + }, + ), + ]), + ExampleModule(title: '组件样式', children: [ + ExampleItem( + ignoreCode: true, + desc: '其他索引类型', + builder: (BuildContext context) { + return const CodeWrapper(builder: _buildOther); + }, + ), + ]), + ], + test: const [], + )); + } +} + +@Demo(group: 'indexes') +Widget _buildSimple(BuildContext context) { + final renderBox = navBarkey.currentContext?.findRenderObject() as RenderBox?; + final indexList = _list.map((item) => item['index'] as String).toList(); + return TDButton( + text: '基础用法', + isBlock: true, + size: TDButtonSize.large, + theme: TDButtonTheme.primary, + type: TDButtonType.outline, + onTap: () { + Navigator.of(context).push( + TDSlidePopupRoute( + slideTransitionFrom: SlideTransitionFrom.right, + modalTop: renderBox?.size.height, + builder: (context) { + return Container( + color: Colors.white, + child: TDIndexes( + indexList: indexList, + builderContent: (context, index) { + final list = _list.firstWhere((element) => element['index'] == index)['children'] as List; + return TDCellGroup( + cells: list + .map((e) => TDCell( + title: e, + )) + .toList(), + ); + }, + ), + ); + }, + ), + ); + }, + ); +} + +@Demo(group: 'indexes') +Widget _buildOther(BuildContext context) { + final renderBox = navBarkey.currentContext?.findRenderObject() as RenderBox?; + final indexList = _list.map((item) => item['index'] as String).toList(); + return TDButton( + text: '胶囊索引', + isBlock: true, + size: TDButtonSize.large, + theme: TDButtonTheme.primary, + type: TDButtonType.outline, + onTap: () { + Navigator.of(context).push( + TDSlidePopupRoute( + slideTransitionFrom: SlideTransitionFrom.right, + modalTop: renderBox?.size.height, + builder: (context) { + return Container( + color: Colors.white, + child: TDIndexes( + indexList: indexList, + capsuleTheme: true, + builderContent: (context, index) { + final list = _list.firstWhere((element) => element['index'] == index)['children'] as List; + return TDCellGroup( + cells: list + .map((e) => TDCell( + title: e, + )) + .toList(), + ); + }, + ), + ); + }, + ), + ); + }, + ); +} \ No newline at end of file diff --git a/tdesign-component/example/lib/page/td_input_page.dart b/tdesign-component/example/lib/page/td_input_page.dart index b055da10a..77c65bb5d 100644 --- a/tdesign-component/example/lib/page/td_input_page.dart +++ b/tdesign-component/example/lib/page/td_input_page.dart @@ -104,6 +104,7 @@ class _TDInputViewPageState extends State { ExampleItem(desc: '长文本样式', builder: _customLongTextStyle), ExampleItem(desc: '隐藏底部分割线', builder: _hideBottomDivider), ExampleItem(desc: '自定义高度-使用SizeBox', builder: _customHeight), + ExampleItem(desc: '获取焦点时点击外部区域事件响应-onTapOutside', builder: _onTapOutside) ], ); } @@ -609,6 +610,8 @@ class _TDInputViewPageState extends State { return Column( children: [ TDInput( + leftInfoWidth: 80, + spacer: TDInputSpacer(iconLabelSpace: 4), leftLabel: '标签超长时最多十个字', controller: controller[18], backgroundColor: Colors.white, @@ -646,6 +649,7 @@ class _TDInputViewPageState extends State { @Demo(group: 'input') Widget _verticalStyle(BuildContext context) { return TDInput( + spacer: TDInputSpacer(iconLabelSpace: 0), type: TDInputType.twoLine, leftLabel: '标签文字', controller: controller[20], @@ -892,4 +896,35 @@ class _TDInputViewPageState extends State { ), ); } + + @Demo(group: 'input') + Widget _onTapOutside(BuildContext context) { + var controller = TextEditingController(); + return Container( + color: Colors.yellow, + alignment: Alignment.center, + height: 90, + child: SizedBox( + height: 60, + child: TDInput( + size: TDInputSize.small, + leftLabel: '标签文字', + controller: controller, + backgroundColor: Colors.white, + hintText: '请输入文字', + onChanged: (text) { + setState(() {}); + }, + onClearTap: () { + controller.clear(); + setState(() {}); + }, + onTapOutside: (event) { + TDToast.showText('点击输入框外部区域', context: context); + print('on tap outside ${event}'); + }, + ), + ), + ); + } } diff --git a/tdesign-component/example/lib/page/td_navbar_page.dart b/tdesign-component/example/lib/page/td_navbar_page.dart index 21c0c8597..99161137a 100644 --- a/tdesign-component/example/lib/page/td_navbar_page.dart +++ b/tdesign-component/example/lib/page/td_navbar_page.dart @@ -62,7 +62,13 @@ class TDNavBarPage extends StatelessWidget { ), ] ) - ] + ], + test: [ + ExampleItem( + desc: '底部阴影', + builder: _shadowNavbar, + ), + ], ); } @@ -256,4 +262,22 @@ class TDNavBarPage extends StatelessWidget { ] ); } + + @Demo(group: 'navbar') + Widget _shadowNavbar(BuildContext context) { + return TDNavBar( + height: 48, + titleFontWeight: FontWeight.w600, + title: titleText, + screenAdaptation: false, + useDefaultBack: true, + boxShadow: [ + BoxShadow( + blurRadius: 4, + offset: const Offset(0, 4), + color: TDTheme.of(context).grayColor5, + ) + ], + ); + } } diff --git a/tdesign-component/example/lib/page/td_notice_bar_page.dart b/tdesign-component/example/lib/page/td_notice_bar_page.dart new file mode 100644 index 000000000..770f9301a --- /dev/null +++ b/tdesign-component/example/lib/page/td_notice_bar_page.dart @@ -0,0 +1,262 @@ +/// @Type Flutter +/// @Author lwb +/// @Date 2024/5/28 + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +import '../annotation/demo.dart'; +import '../base/example_widget.dart'; + +class TDNoticeBarPage extends StatelessWidget { + const TDNoticeBarPage({super.key}); + + @override + Widget build(BuildContext context) { + return ExamplePage( + title: tdTitle(context), + exampleCodeGroup: 'noticeBar', + desc: '在导航栏下方,用于给用户显示提示消息。', + backgroundColor: Colors.white, + children: [ + ExampleModule(title: '组件类型', children: [ + ExampleItem(desc: '纯文字的公告栏', builder: _textNoticeBar), + ExampleItem(desc: '可滚动的公告栏', builder: _scrollNoticeBar), + ExampleItem(builder: _scrollIconNoticeBar), + ExampleItem(desc: '带图标的公告栏', builder: _iconNoticeBar), + ExampleItem(desc: '带关闭的公告栏', builder: _closeNoticeBar), + ExampleItem(desc: '带入口的公告栏', builder: _entranceNoticeBar1), + ExampleItem(builder: _entranceNoticeBar2), + ExampleItem(desc: '自定义样式的公告栏', builder: _customNoticeBar), + ]), + ExampleModule(title: '组件状态', children: [ + ExampleItem(desc: '普通通知', builder: _normalNoticeBar), + ExampleItem(desc: '成功通知', builder: _successNoticeBar), + ExampleItem(desc: '警示通知', builder: _warningNoticeBar), + ExampleItem(desc: '错误通知', builder: _errorNoticeBar), + ]), + ExampleModule(title: '组件样式', children: [ + ExampleItem(desc: '卡片顶部', builder: _cardNoticeBar), + ]) + ], + test: [ + ExampleItem(desc: '带点击事件的公告栏', builder: _tapNoticeBar), + ExampleItem(desc: '自定义左侧内容的公告栏', builder: _leftNoticeBar), + ExampleItem(desc: '垂直滚动的公告栏', builder: _stepNoticeBar), + ], + ); + } +} + +@Demo(group: 'noticeBar') +Widget _textNoticeBar(BuildContext context) { + return const TDNoticeBar(context: '这是一条普通的通知信息'); +} + +@Demo(group: 'noticeBar') +Widget _scrollNoticeBar(BuildContext context) { + return const TDNoticeBar( + context: '提示文字描述提示文字描述提示文字描述提示文字描述提示文字', + marquee: true, + speed: 50, + ); +} + +@Demo(group: 'noticeBar') +Widget _scrollIconNoticeBar(BuildContext context) { + return const Padding( + padding: EdgeInsets.only(top: 16), + child: TDNoticeBar( + context: '提示文字描述提示文字描述提示文字描述提示文字描述提示文字', + speed: 50, + prefixIcon: TDIcons.sound, + marquee: true, + ), + ); +} + +@Demo(group: 'noticeBar') +Widget _iconNoticeBar(BuildContext context) { + return const TDNoticeBar( + context: '这是一条普通的通知信息', + prefixIcon: TDIcons.error_circle_filled, + ); +} + +@Demo(group: 'noticeBar') +Widget _closeNoticeBar(BuildContext context) { + return const TDNoticeBar( + context: '这是一条普通的通知信息', + prefixIcon: TDIcons.error_circle_filled, + suffixIcon: TDIcons.close, + ); +} + +@Demo(group: 'noticeBar') +Widget _entranceNoticeBar1(BuildContext context) { + return const TDNoticeBar( + context: '这是一条普通的通知信息', + prefixIcon: TDIcons.error_circle_filled, + right: TDButton( + text: '文字按钮', + type: TDButtonType.text, + theme: TDButtonTheme.primary, + size: TDButtonSize.extraSmall, + height: 22, + padding: EdgeInsets.symmetric(vertical: 0, horizontal: 0), + ), + ); +} + +@Demo(group: 'noticeBar') +Widget _entranceNoticeBar2(BuildContext context) { + return const Padding( + padding: EdgeInsets.only(top: 16), + child: TDNoticeBar( + context: '这是一条普通的通知信息', + prefixIcon: TDIcons.error_circle_filled, + suffixIcon: TDIcons.chevron_right, + ), + ); +} + +@Demo(group: 'noticeBar') +Widget _customNoticeBar(BuildContext context) { + return TDNoticeBar( + context: '这是一条普通的通知信息', + prefixIcon: TDIcons.notification, + suffixIcon: TDIcons.chevron_right, + style: TDNoticeBarStyle(backgroundColor: TDTheme.of(context).grayColor3), + ); +} + +@Demo(group: 'noticeBar') +Widget _normalNoticeBar(BuildContext context) { + return const TDNoticeBar( + context: '这是一条普通的通知信息', + prefixIcon: TDIcons.error_circle_filled, + theme: TDNoticeBarTheme.info, + ); +} + +@Demo(group: 'noticeBar') +Widget _successNoticeBar(BuildContext context) { + return const TDNoticeBar( + context: '这是一条普通的通知信息', + prefixIcon: TDIcons.error_circle_filled, + theme: TDNoticeBarTheme.success, + ); +} + +@Demo(group: 'noticeBar') +Widget _warningNoticeBar(BuildContext context) { + return const TDNoticeBar( + context: '这是一条普通的通知信息', + prefixIcon: TDIcons.error_circle_filled, + theme: TDNoticeBarTheme.warning, + ); +} + +@Demo(group: 'noticeBar') +Widget _errorNoticeBar(BuildContext context) { + return const TDNoticeBar( + context: '这是一条普通的通知信息', + prefixIcon: TDIcons.error_circle_filled, + theme: TDNoticeBarTheme.error, + ); +} + +@Demo(group: 'noticeBar') +Widget _cardNoticeBar(BuildContext context) { + var size = MediaQuery.of(context).size; + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: TDNoticeBarStyle.generateTheme(context).backgroundColor, + borderRadius: const BorderRadius.all(Radius.circular(9)), + boxShadow: const [ + BoxShadow( + color: Color(0x0d000000), + blurRadius: 8, + spreadRadius: 2, + offset: Offset(0, 2), + ), + BoxShadow( + color: Color(0x0f000000), + blurRadius: 10, + spreadRadius: 1, + offset: Offset(0, 8), + ), + BoxShadow( + color: Color(0x1a000000), + blurRadius: 5, + spreadRadius: -3, + offset: Offset(0, 5), + ), + ], + ), + child: Column( + children: [ + Container( + width: size.width - 32, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + clipBehavior: Clip.hardEdge, + child: const TDNoticeBar( + context: '这是一条普通的通知信息', + prefixIcon: TDIcons.error_circle_filled, + suffixIcon: TDIcons.chevron_right, + ), + ), + Container( + height: 150, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ) + ], + ), + ); +} + +@Demo(group: 'noticeBar') +Widget _tapNoticeBar(BuildContext context) { + return TDNoticeBar( + context: '这是一条普通的通知信息', + prefixIcon: TDIcons.error_circle_filled, + suffixIcon: TDIcons.chevron_right, + onTap: (trigger) { + TDToast.showText('tap:$trigger', context: context); + }, + ); +} + +@Demo(group: 'noticeBar') +Widget _leftNoticeBar(BuildContext context) { + return const TDNoticeBar( + context: '这是一条普通的通知信息', + suffixIcon: TDIcons.chevron_right, + left: TDButton( + text: '文本', + type: TDButtonType.text, + theme: TDButtonTheme.primary, + size: TDButtonSize.extraSmall, + height: 22, + padding: EdgeInsets.symmetric(vertical: 0, horizontal: 0), + ), + ); +} + +@Demo(group: 'noticeBar') +Widget _stepNoticeBar(BuildContext context) { + return const TDNoticeBar( + context: ['君不见黄河之水天上来', '奔流到海不复回', '君不见'], + direction: Axis.vertical, + prefixIcon: TDIcons.sound, + marquee: true, + ); +} diff --git a/tdesign-component/example/lib/page/td_picker_page.dart b/tdesign-component/example/lib/page/td_picker_page.dart index fdc903c40..2dcd254f3 100644 --- a/tdesign-component/example/lib/page/td_picker_page.dart +++ b/tdesign-component/example/lib/page/td_picker_page.dart @@ -72,7 +72,11 @@ class _TDPickerPageState extends State { ExampleItem(desc: '无标题选择器', builder: buildAreaWithoutTitle), ], ) - ] + ], + test: [ + ExampleItem( + desc: '自定义left/right text', builder: buildCustomLeftRightText), + ], ); } @@ -159,6 +163,41 @@ class _TDPickerPageState extends State { ); } + @Demo(group: 'picker') + Widget buildCustomLeftRightText(BuildContext context) { + return Column( + children: [ + GestureDetector( + onTap: () { + TDPicker.showMultiPicker(context, + leftText: '自定义取消', + rightText: '自定义确认', + title: '基础选择器', onConfirm: (selected) { + setState(() { + selected_5 = '${data_1[selected[0]]}'; + }); + Navigator.of(context).pop(); + }, data: [data_1]); + }, + child: buildSelectRow(context, selected_5, '基础选择器'), + ), + GestureDetector( + onTap: () { + TDPicker.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: ['浙江省', '杭州市', '西湖区']); + }, + child: buildSelectRow(context, selected_3, '联动选择器'), + ) + ], + ); + } Widget buildSelectRow(BuildContext context, String output, String title) { return Container( diff --git a/tdesign-component/example/lib/page/td_popup_page.dart b/tdesign-component/example/lib/page/td_popup_page.dart index 263359c08..ec597af03 100644 --- a/tdesign-component/example/lib/page/td_popup_page.dart +++ b/tdesign-component/example/lib/page/td_popup_page.dart @@ -305,6 +305,128 @@ class TDPopupPageState extends State { ); }, ), + ExampleItem( + desc: '弹出层包含输入框且会被键盘遮挡', + builder: (_) { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Container( + margin: const EdgeInsets.all(8), + child: TDButton( + text: '底部弹出层-被键盘弹出遮挡', + isBlock: true, + theme: TDButtonTheme.primary, + type: TDButtonType.outline, + size: TDButtonSize.large, + onTap: () { + Navigator.of(context).push(TDSlidePopupRoute( + modalBarrierColor: TDTheme.of(context).fontGyColor2, + slideTransitionFrom: SlideTransitionFrom.bottom, + builder: (context) { + return TDPopupBottomDisplayPanel( + title: '标题文字标题文字标题文字标题文字标题文字标题文字标题文字', + closeColor: TDTheme.of(context).errorNormalColor, + closeClick: () { + Navigator.maybePop(context); + }, + child: Material( + child: SizedBox( + height: 100, + child: TDInput( + type: TDInputType.normal, + leftLabel: '标签文字', + hintText: '请输入文字', + maxLength: 10, + additionInfo: '最大输入10个字符', + backgroundColor: Colors.white, + ), + ), + ), + radius: 6, + ); + })); + }, + ), + ), + Container( + margin: const EdgeInsets.all(8), + child: TDButton( + text: '居中弹出层-被键盘弹出遮挡', + isBlock: true, + theme: TDButtonTheme.primary, + type: TDButtonType.outline, + size: TDButtonSize.large, + onTap: () { + Navigator.of(context).push(TDSlidePopupRoute( + modalBarrierColor: TDTheme.of(context).fontGyColor2, + slideTransitionFrom: SlideTransitionFrom.center, + builder: (context) { + return TDPopupCenterPanel( + closeColor: TDTheme.of(context).errorNormalColor, + closeClick: () { + Navigator.maybePop(context); + }, + child: Material( + child: SizedBox( + height: 336, + child: Column( + children: [ + TDInput( + type: TDInputType.normal, + leftLabel: '标签文字1', + hintText: '请输入文字1', + maxLength: 10, + backgroundColor: Colors.white, + ), + TDInput( + type: TDInputType.normal, + leftLabel: '标签文字2', + hintText: '请输入文字2', + maxLength: 10, + backgroundColor: Colors.white, + ), + TDInput( + type: TDInputType.normal, + leftLabel: '标签文字3', + hintText: '请输入文字3', + maxLength: 10, + backgroundColor: Colors.white, + ), + TDInput( + type: TDInputType.normal, + leftLabel: '标签文字4', + hintText: '请输入文字4', + maxLength: 10, + backgroundColor: Colors.white, + ), + TDInput( + type: TDInputType.normal, + leftLabel: '会被键盘遮挡的输入框1', + hintText: '会被键盘遮挡小部分', + maxLength: 10, + backgroundColor: Colors.white, + ), + TDInput( + type: TDInputType.normal, + leftLabel: '会被键盘遮挡的输入框2', + hintText: '会被键盘遮挡全遮挡', + maxLength: 10, + backgroundColor: Colors.white, + ) + ], + ), + ), + ), + radius: 6, + ); + })); + }, + )), + ], + ); + }) ], ); } diff --git a/tdesign-component/example/lib/page/td_search_bar_page.dart b/tdesign-component/example/lib/page/td_search_bar_page.dart index 42702a367..85cba87b0 100644 --- a/tdesign-component/example/lib/page/td_search_bar_page.dart +++ b/tdesign-component/example/lib/page/td_search_bar_page.dart @@ -14,6 +14,7 @@ class TDSearchBarPage extends StatefulWidget { class _TDSearchBarPageState extends State { String? inputText; String? searchText; + TextEditingController inputController = TextEditingController(); @override Widget build(BuildContext context) { @@ -37,6 +38,7 @@ class _TDSearchBarPageState extends State { ], test: [ ExampleItem(desc: '获取焦点后显示自定义操作按钮', builder: _buildSearchBarWithAction), + ExampleItem(desc: '自定义获取焦点后显示按钮', builder: _buildFocusSearchBarWithAction), ],); } @@ -150,4 +152,27 @@ class _TDSearchBarPageState extends State { ], ); } + + @Demo(group: 'search') + Widget _buildFocusSearchBarWithAction(BuildContext context) { + return TDSearchBar( + placeHolder: '搜索预设文案', + action: '搜索', + needCancel: true, + controller: inputController, + onActionClick: () { + showGeneralDialog( + context: context, + pageBuilder: (BuildContext buildContext, Animation animation, + Animation secondaryAnimation) { + return TDConfirmDialog( + content: inputController.text.isNotEmpty + ? '搜索关键词:${inputController.text}' + : '搜索关键词为空', + ); + }, + ); + }, + ); + } } diff --git a/tdesign-component/example/lib/page/td_count_down_page.dart b/tdesign-component/example/lib/page/td_time_counter_page.dart similarity index 70% rename from tdesign-component/example/lib/page/td_count_down_page.dart rename to tdesign-component/example/lib/page/td_time_counter_page.dart index e1a07a1a8..a1272d54a 100644 --- a/tdesign-component/example/lib/page/td_count_down_page.dart +++ b/tdesign-component/example/lib/page/td_time_counter_page.dart @@ -3,8 +3,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../base/example_widget.dart'; import '../annotation/demo.dart'; -class TDCountDownPage extends StatelessWidget { - const TDCountDownPage({super.key}); +class TDTimeCounterPage extends StatelessWidget { + const TDTimeCounterPage({super.key}); @override Widget build(BuildContext context) { @@ -12,8 +12,8 @@ class TDCountDownPage extends StatelessWidget { color: TDTheme.of(context).grayColor2, child: ExamplePage( title: tdTitle(context), - desc: '用于实时展示倒计时数值。', - exampleCodeGroup: 'countDown', + desc: '用于实时展示计时数值。', + exampleCodeGroup: 'timeCounter', children: [ ExampleModule(title: '组件类型', children: [ ExampleItem( @@ -34,6 +34,15 @@ class TDCountDownPage extends StatelessWidget { return const CodeWrapper(builder: _buildMillisecondSimple); }, ), + ExampleItem( + ignoreCode: true, + desc: '正向计时', + center: false, + padding: const EdgeInsets.only(left: 16), + builder: (BuildContext context) { + return const CodeWrapper(builder: _buildUpSimple); + }, + ), ExampleItem( ignoreCode: true, desc: '带方形底', @@ -284,188 +293,206 @@ class TDCountDownPage extends StatelessWidget { return const CodeWrapper(builder: _buildControl); }, ), + ExampleItem( + ignoreCode: true, + desc: '自定义显示位数', + center: false, + padding: const EdgeInsets.only(left: 16), + builder: (BuildContext context) { + return const CodeWrapper(builder: _buildCustomNum); + }, + ), ], ), ); } } -@Demo(group: 'countDown') -TDCountDown _buildSimple(BuildContext context) { - return const TDCountDown(time: 60 * 60 * 1000); +@Demo(group: 'timeCounter') +TDTimeCounter _buildSimple(BuildContext context) { + return const TDTimeCounter(time: 60 * 60 * 1000); +} + +@Demo(group: 'timeCounter') +TDTimeCounter _buildMillisecondSimple(BuildContext context) { + return const TDTimeCounter(time: 60 * 60 * 1000, millisecond: true); } -@Demo(group: 'countDown') -TDCountDown _buildMillisecondSimple(BuildContext context) { - return const TDCountDown(time: 60 * 60 * 1000, millisecond: true); +@Demo(group: 'timeCounter') +TDTimeCounter _buildUpSimple(BuildContext context) { + return const TDTimeCounter( + time: 60 * 60 * 1000, + millisecond: true, + direction: TDTimeCounterDirection.up, + ); } -@Demo(group: 'countDown') -TDCountDown _buildSquareSimple(BuildContext context) { - return const TDCountDown(time: 60 * 60 * 1000, theme: TDCountDownTheme.square); +@Demo(group: 'timeCounter') +TDTimeCounter _buildSquareSimple(BuildContext context) { + return const TDTimeCounter(time: 60 * 60 * 1000, theme: TDTimeCounterTheme.square); } -@Demo(group: 'countDown') -TDCountDown _buildRoundSimple(BuildContext context) { - return const TDCountDown(time: 60 * 60 * 1000, theme: TDCountDownTheme.round); +@Demo(group: 'timeCounter') +TDTimeCounter _buildRoundSimple(BuildContext context) { + return const TDTimeCounter(time: 60 * 60 * 1000, theme: TDTimeCounterTheme.round); } -@Demo(group: 'countDown') -TDCountDown _buildUnitSimple(BuildContext context) { - return const TDCountDown(time: 60 * 60 * 1000, theme: TDCountDownTheme.square, splitWithUnit: true); +@Demo(group: 'timeCounter') +TDTimeCounter _buildUnitSimple(BuildContext context) { + return const TDTimeCounter(time: 60 * 60 * 1000, theme: TDTimeCounterTheme.square, splitWithUnit: true); } -@Demo(group: 'countDown') -TDCountDown _buildCustomUnitSimple(BuildContext context) { - var style = TDCountDownStyle.generateStyle(context); +@Demo(group: 'timeCounter') +TDTimeCounter _buildCustomUnitSimple(BuildContext context) { + var style = TDTimeCounterStyle.generateStyle(context); style.timeColor = TDTheme.of(context).errorColor6; - return TDCountDown(time: 60 * 60 * 1000, splitWithUnit: true, style: style); + return TDTimeCounter(time: 60 * 60 * 1000, splitWithUnit: true, style: style); } -@Demo(group: 'countDown') -TDCountDown _buildSmallSize(BuildContext context) { - return const TDCountDown( +@Demo(group: 'timeCounter') +TDTimeCounter _buildSmallSize(BuildContext context) { + return const TDTimeCounter( time: 60 * 60 * 1000, - size: TDCountDownSize.small, + size: TDTimeCounterSize.small, ); } -@Demo(group: 'countDown') -TDCountDown _buildMediumSize(BuildContext context) { - return const TDCountDown( +@Demo(group: 'timeCounter') +TDTimeCounter _buildMediumSize(BuildContext context) { + return const TDTimeCounter( time: 60 * 60 * 1000, - size: TDCountDownSize.medium, + size: TDTimeCounterSize.medium, ); } -@Demo(group: 'countDown') -TDCountDown _buildLargeSize(BuildContext context) { - return const TDCountDown( +@Demo(group: 'timeCounter') +TDTimeCounter _buildLargeSize(BuildContext context) { + return const TDTimeCounter( time: 60 * 60 * 1000, - size: TDCountDownSize.large, + size: TDTimeCounterSize.large, ); } -@Demo(group: 'countDown') -TDCountDown _buildSquareSmallSize(BuildContext context) { - return const TDCountDown( +@Demo(group: 'timeCounter') +TDTimeCounter _buildSquareSmallSize(BuildContext context) { + return const TDTimeCounter( time: 60 * 60 * 1000, - size: TDCountDownSize.small, - theme: TDCountDownTheme.square, + size: TDTimeCounterSize.small, + theme: TDTimeCounterTheme.square, ); } -@Demo(group: 'countDown') -TDCountDown _buildSquareMediumSize(BuildContext context) { - return const TDCountDown( +@Demo(group: 'timeCounter') +TDTimeCounter _buildSquareMediumSize(BuildContext context) { + return const TDTimeCounter( time: 60 * 60 * 1000, - size: TDCountDownSize.medium, - theme: TDCountDownTheme.square, + size: TDTimeCounterSize.medium, + theme: TDTimeCounterTheme.square, ); } -@Demo(group: 'countDown') -TDCountDown _buildSquareLargeSize(BuildContext context) { - return const TDCountDown( +@Demo(group: 'timeCounter') +TDTimeCounter _buildSquareLargeSize(BuildContext context) { + return const TDTimeCounter( time: 60 * 60 * 1000, - size: TDCountDownSize.large, - theme: TDCountDownTheme.square, + size: TDTimeCounterSize.large, + theme: TDTimeCounterTheme.square, ); } -@Demo(group: 'countDown') -TDCountDown _buildRoundSmallSize(BuildContext context) { - return const TDCountDown( +@Demo(group: 'timeCounter') +TDTimeCounter _buildRoundSmallSize(BuildContext context) { + return const TDTimeCounter( time: 60 * 60 * 1000, - size: TDCountDownSize.small, - theme: TDCountDownTheme.round, + size: TDTimeCounterSize.small, + theme: TDTimeCounterTheme.round, ); } -@Demo(group: 'countDown') -TDCountDown _buildRoundMediumSize(BuildContext context) { - return const TDCountDown( +@Demo(group: 'timeCounter') +TDTimeCounter _buildRoundMediumSize(BuildContext context) { + return const TDTimeCounter( time: 60 * 60 * 1000, - size: TDCountDownSize.medium, - theme: TDCountDownTheme.round, + size: TDTimeCounterSize.medium, + theme: TDTimeCounterTheme.round, ); } -@Demo(group: 'countDown') -TDCountDown _buildRoundLargeSize(BuildContext context) { - return const TDCountDown( +@Demo(group: 'timeCounter') +TDTimeCounter _buildRoundLargeSize(BuildContext context) { + return const TDTimeCounter( time: 60 * 60 * 1000, - size: TDCountDownSize.large, - theme: TDCountDownTheme.round, + size: TDTimeCounterSize.large, + theme: TDTimeCounterTheme.round, ); } -@Demo(group: 'countDown') -TDCountDown _buildUnitSmallSize(BuildContext context) { - return const TDCountDown( +@Demo(group: 'timeCounter') +TDTimeCounter _buildUnitSmallSize(BuildContext context) { + return const TDTimeCounter( time: 60 * 60 * 1000, - size: TDCountDownSize.small, - theme: TDCountDownTheme.square, + size: TDTimeCounterSize.small, + theme: TDTimeCounterTheme.square, splitWithUnit: true, ); } -@Demo(group: 'countDown') -TDCountDown _buildUnitMediumSize(BuildContext context) { - return const TDCountDown( +@Demo(group: 'timeCounter') +TDTimeCounter _buildUnitMediumSize(BuildContext context) { + return const TDTimeCounter( time: 60 * 60 * 1000, - size: TDCountDownSize.medium, - theme: TDCountDownTheme.square, + size: TDTimeCounterSize.medium, + theme: TDTimeCounterTheme.square, splitWithUnit: true, ); } -@Demo(group: 'countDown') -TDCountDown _buildUnitLargeSize(BuildContext context) { - return const TDCountDown( +@Demo(group: 'timeCounter') +TDTimeCounter _buildUnitLargeSize(BuildContext context) { + return const TDTimeCounter( time: 60 * 60 * 1000, - size: TDCountDownSize.large, - theme: TDCountDownTheme.square, + size: TDTimeCounterSize.large, + theme: TDTimeCounterTheme.square, splitWithUnit: true, ); } -@Demo(group: 'countDown') -TDCountDown _buildCustomUnitSmallSize(BuildContext context) { - var style = TDCountDownStyle.generateStyle(context, size: TDCountDownSize.small); +@Demo(group: 'timeCounter') +TDTimeCounter _buildCustomUnitSmallSize(BuildContext context) { + var style = TDTimeCounterStyle.generateStyle(context, size: TDTimeCounterSize.small); style.timeColor = TDTheme.of(context).errorColor6; - return TDCountDown( + return TDTimeCounter( time: 60 * 60 * 1000, splitWithUnit: true, style: style, ); } -@Demo(group: 'countDown') -TDCountDown _buildCustomUnitMediumSize(BuildContext context) { - var style = TDCountDownStyle.generateStyle(context, size: TDCountDownSize.medium); +@Demo(group: 'timeCounter') +TDTimeCounter _buildCustomUnitMediumSize(BuildContext context) { + var style = TDTimeCounterStyle.generateStyle(context, size: TDTimeCounterSize.medium); style.timeColor = TDTheme.of(context).errorColor6; - return TDCountDown( + return TDTimeCounter( time: 60 * 60 * 1000, splitWithUnit: true, style: style, ); } -@Demo(group: 'countDown') -TDCountDown _buildCustomUnitLargeSize(BuildContext context) { - var style = TDCountDownStyle.generateStyle(context, size: TDCountDownSize.large); +@Demo(group: 'timeCounter') +TDTimeCounter _buildCustomUnitLargeSize(BuildContext context) { + var style = TDTimeCounterStyle.generateStyle(context, size: TDTimeCounterSize.large); style.timeColor = TDTheme.of(context).errorColor6; - return TDCountDown( + return TDTimeCounter( time: 60 * 60 * 1000, splitWithUnit: true, style: style, ); } -@Demo(group: 'countDown') +@Demo(group: 'timeCounter') Widget _buildControl(BuildContext context) { - var controller = TDCountDownController(); + var controller = TDTimeCounterController(); return Wrap( direction: Axis.vertical, spacing: 8, @@ -515,11 +542,19 @@ Widget _buildControl(BuildContext context) { ), ], ), - TDCountDown( + TDTimeCounter( time: 60 * 60 * 1000, controller: controller, - autoStart: false, + // autoStart: false, ), ], ); } + +@Demo(group: 'timeCounter') +TDTimeCounter _buildCustomNum(BuildContext context) { + return const TDTimeCounter( + time: 2000 * 60 * 1000, + format: 'mmmmmmm分sss秒', + ); +} diff --git a/tdesign-component/example/lib/page/td_toast_page.dart b/tdesign-component/example/lib/page/td_toast_page.dart index afa96a125..20366f2a1 100644 --- a/tdesign-component/example/lib/page/td_toast_page.dart +++ b/tdesign-component/example/lib/page/td_toast_page.dart @@ -35,7 +35,11 @@ class _TDToastPageState extends State { ExampleItem(desc: '失败提示', builder: _failToast), ExampleItem(desc: '失败提示(竖向)', builder: _failVerticalToast), ]) - ]); + ], + test: [ + ExampleItem(desc: '禁止滚动+点击', builder: _preventTapToast), + ], + ); } @Demo(group: 'toast') @@ -226,4 +230,19 @@ class _TDToastPageState extends State { text: '失败提示(竖向)', ); } + + @Demo(group: 'toast') + Widget _preventTapToast(BuildContext context) { + return TDButton( + onTap: () { + TDToast.showText('轻提示文字内容', + context: context, preventTap: true); + }, + size: TDButtonSize.large, + type: TDButtonType.outline, + theme: TDButtonTheme.primary, + isBlock: true, + text: '禁止滚动+点击', + ); + } } diff --git a/tdesign-component/lib/src/components/button/td_button.dart b/tdesign-component/lib/src/components/button/td_button.dart index db4070f6f..38157c190 100644 --- a/tdesign-component/lib/src/components/button/td_button.dart +++ b/tdesign-component/lib/src/components/button/td_button.dart @@ -327,11 +327,11 @@ class _TDButtonState extends State { case TDButtonSize.large: return 24; case TDButtonSize.medium: - return 22; - case TDButtonSize.small: return 20; + case TDButtonSize.small: + return 18; case TDButtonSize.extraSmall: - return 20; + return 14; } } diff --git a/tdesign-component/lib/src/components/calendar/td_calendar.dart b/tdesign-component/lib/src/components/calendar/td_calendar.dart new file mode 100644 index 000000000..e84cb7a0a --- /dev/null +++ b/tdesign-component/lib/src/components/calendar/td_calendar.dart @@ -0,0 +1,328 @@ +import 'package:flutter/material.dart'; +import '../../../tdesign_flutter.dart'; +import '../../util/context_extension.dart'; +import '../../util/iterable_ext.dart'; + +export 'td_calendar_body.dart'; +export 'td_calendar_cell.dart'; +export 'td_calendar_header.dart'; +export 'td_calendar_popup.dart'; +export 'td_calendar_style.dart'; + +typedef CalendarFormat = TDate? Function(TDate? day); + +enum CalendarType { single, multiple, range } + +enum CalendarTrigger { closeBtn, confirmBtn, overlay } + +enum DateSelectType { selected, disabled, start, centre, end, empty } + +/// 日历组件 +class TDCalendar extends StatefulWidget { + const TDCalendar({ + Key? key, + this.firstDayOfWeek = 0, + this.format, + this.maxDate, + this.minDate, + this.title, + this.titleWidget, + this.type = CalendarType.single, + this.value, + this.displayFormat = 'year month', + this.cellHeight = 60, + this.height, + this.width, + this.style, + this.onChange, + this.onCellClick, + this.onCellLongPress, + this.onHeanderClick, + this.useTimePicker = false, + this.timePickerModel, + }) : super(key: key); + + /// 第一天从星期几开始,默认 0 = 周日 + final int? firstDayOfWeek; + + /// 用于格式化日期的函数,可定义日期前后的显示内容和日期样式 + final CalendarFormat? format; + + /// 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认半年后 + final int? maxDate; + + /// 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认今天 + final int? minDate; + + /// 标题 + final String? title; + + /// 标题组件 + final Widget? titleWidget; + + /// 日历的选择类型,single = 单选;multiple = 多选; range = 区间选择 + final CalendarType? type; + + /// 当前选择的日期(fromMillisecondsSinceEpoch),不传则默认今天,当 type = single 时数组长度为1 + final List? value; + + /// 年月显示格式,`year`表示年,`month`表示月,如`year month`表示年在前、月在后、中间隔一个空格 + final String? displayFormat; + + /// 高度 + final double? height; + + /// 日期高度 + final double? cellHeight; + + /// 宽度 + final double? width; + + /// 自定义样式 + final TDCalendarStyle? style; + + /// 选中值变化时触发 + final void Function(List value)? onChange; + + /// 点击日期时触发 + final void Function(int value, DateSelectType type, TDate tdate)? onCellClick; + + /// 长安日期时触发 + final void Function(int value, DateSelectType type, TDate tdate)? onCellLongPress; + + /// 点击周时触发 + final void Function(int index, String week)? onHeanderClick; + + /// 是否显示时间选择器 + final bool? useTimePicker; + + /// 自定义时间选择器 + final List? timePickerModel; + + List? get _value => value?.map((e) { + final date = DateTime.fromMillisecondsSinceEpoch(e); + return DateTime(date.year, date.month, date.day); + }).toList(); + + List? get _valueTime => value?.map(DateTime.fromMillisecondsSinceEpoch).toList(); + + @override + _TDCalendarState createState() => _TDCalendarState(); +} + +class _TDCalendarState extends State { + late List weekdayNames; + late List monthNames; + late TDCalendarInherited? inherited; + late TDCalendarStyle _style; + final List timePickerModelList = []; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + weekdayNames = [ + context.resource.sunday, + context.resource.monday, + context.resource.tuesday, + context.resource.wednesday, + context.resource.thursday, + context.resource.friday, + context.resource.saturday, + ]; + monthNames = [ + context.resource.january, + context.resource.february, + context.resource.march, + context.resource.april, + context.resource.may, + context.resource.june, + context.resource.july, + context.resource.august, + context.resource.september, + context.resource.october, + context.resource.november, + context.resource.december, + ]; + _style = widget.style ?? TDCalendarStyle.generateStyle(context); + } + + @override + void didUpdateWidget(TDCalendar oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.value != oldWidget.value) { + inherited?.selected.value = widget.value ?? []; + } + } + + @override + Widget build(BuildContext context) { + inherited = TDCalendarInherited.of(context); + _initValue(); + timePickerModelList.clear(); + final verticalGap = TDTheme.of(context).spacer8; + return Container( + height: widget.height, + width: widget.width ?? double.infinity, + decoration: _style.decoration, + child: Column( + children: [ + TDCalendarHeader( + firstDayOfWeek: widget.firstDayOfWeek ?? 0, + weekdayGap: TDTheme.of(context).spacer4, + padding: TDTheme.of(context).spacer16, + weekdayStyle: _style.weekdayStyle, + weekdayHeight: 46, + title: widget.title, + titleStyle: _style.titleStyle, + titleWidget: widget.titleWidget, + titleMaxLine: _style.titleMaxLine, + titleOverflow: TextOverflow.ellipsis, + closeBtn: inherited?.usePopup ?? false, + closeColor: _style.titleCloseColor, + weekdayNames: weekdayNames, + onClose: inherited?.onClose, + onClick: widget.onHeanderClick, + ), + Expanded( + child: TDCalendarBody( + type: widget.type ?? CalendarType.single, + firstDayOfWeek: widget.firstDayOfWeek ?? 0, + maxDate: widget.maxDate, + minDate: widget.minDate, + value: widget._value, + bodyPadding: TDTheme.of(context).spacer16, + displayFormat: widget.displayFormat ?? 'year month', + monthNames: monthNames, + monthTitleStyle: _style.monthTitleStyle, + verticalGap: verticalGap, + builder: (date, dateList, data, rowIndex, colIndex) { + return TDCalendarCell( + height: widget.cellHeight ?? 60, + tdate: date, + format: widget.format, + type: widget.type ?? CalendarType.single, + data: data, + padding: verticalGap / 2, + onChange: (value) { + final time = _getValue(value); + inherited?.selected.value = time; + widget.onChange?.call(time); + }, + onCellClick: widget.onCellClick, + onCellLongPress: widget.onCellLongPress, + dateList: dateList, + rowIndex: rowIndex, + colIndex: colIndex, + ); + }, + ), + ), + if (widget.useTimePicker == true) _getTimePicker(), + if (inherited?.usePopup == true) + inherited?.confirmBtn ?? + Padding( + padding: EdgeInsets.symmetric(vertical: TDTheme.of(context).spacer16), + child: TDButton( + theme: TDButtonTheme.primary, + text: '确定', + isBlock: true, + size: TDButtonSize.large, + onTap: inherited?.onConfirm, + ), + ), + ], + ), + ); + } + + Widget _getTimePicker() { + final noRange = widget.type != CalendarType.range; + final now = DateTime.now(); + final valueTime = widget._valueTime; + return Container( + decoration: BoxDecoration( + color: TDTheme.of(context).whiteColor1, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Row( + children: List.generate( + noRange ? 1 : 2, + (index) { + final timePickerModel = widget.timePickerModel?.getOrNull(index) ?? + DatePickerModel( + useYear: false, + useMonth: false, + useDay: false, + useWeekDay: false, + useHour: true, + useMinute: true, + useSecond: false, + dateStart: [1999, 01, 01], + dateEnd: [2999, 12, 31], + dateInitial: [ + ...[1999, 01, 01], + valueTime?.getOrNull(index)?.hour ?? now.hour, + valueTime?.getOrNull(index)?.minute ?? now.minute, + valueTime?.getOrNull(index)?.second ?? now.second + ], + ); + final timePicker = TDDatePicker( + title: noRange + ? context.resource.time + : index == 0 + ? context.resource.start + : context.resource.end, + leftText: '', + rightText: '', + model: timePickerModel, + pickerHeight: 178, + pickerItemCount: 3, + onConfirm: (Map selected) {}, + onSelectedItemChanged: (index) { + final time = _getValue(inherited?.selected.value ?? []); + inherited?.selected.value = time; + widget.onChange?.call(time); + }, + ); + timePickerModelList.add(timePickerModel); + return Expanded(child: timePicker); + }, + ), + ), + ); + } + + List _getValue(List value) { + final dateValue = value.map((e) { + final date = DateTime.fromMillisecondsSinceEpoch(e); + return DateTime(date.year, date.month, date.day).millisecondsSinceEpoch; + }).toList(); + if (widget.useTimePicker != true) { + return dateValue; + } + final milliseconds = timePickerModelList.map((model) { + final hour = model.useHour ? model.hourFixedExtentScrollController.selectedItem : 0; + final minute = model.useMinute ? model.minuteFixedExtentScrollController.selectedItem : 0; + final second = model.useSecond ? model.secondFixedExtentScrollController.selectedItem : 0; + return (hour * 60 * 60 + minute * 60 + second) * 1000; + }).toList(); + return dateValue.mapWidthIndex((e, index) { + if (widget.type != CalendarType.range) { + return e + (milliseconds.getOrNull(0) ?? 0); + } + return e + (milliseconds.getOrNull(index) ?? 0); + }).toList(); + } + + void _initValue() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + inherited?.selected.value = _getValue(widget.value ?? []); + }); + } +} diff --git a/tdesign-component/lib/src/components/calendar/td_calendar_body.dart b/tdesign-component/lib/src/components/calendar/td_calendar_body.dart new file mode 100644 index 000000000..f78005806 --- /dev/null +++ b/tdesign-component/lib/src/components/calendar/td_calendar_body.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; +import '../../../tdesign_flutter.dart'; +import '../../util/context_extension.dart'; +import '../../util/iterable_ext.dart'; + +class TDCalendarBody extends StatelessWidget { + const TDCalendarBody({ + Key? key, + this.maxDate, + this.minDate, + required this.type, + this.value, + required this.firstDayOfWeek, + required this.builder, + required this.bodyPadding, + required this.displayFormat, + required this.monthNames, + this.monthTitleStyle, + required this.verticalGap, + }) : super(key: key); + + final int? maxDate; + final int? minDate; + final CalendarType type; + final List? value; + final int firstDayOfWeek; + final Widget Function( + TDate? date, List dateList, Map> data, int rowIndex, int colIndex) builder; + final double bodyPadding; + final String displayFormat; + final List monthNames; + final TextStyle? monthTitleStyle; + final double verticalGap; + + @override + Widget build(BuildContext context) { + final itemKey = GlobalKey(); + final scrollController = ScrollController(); + var scrollToIndex = 0; + final min = _getDefDate(minDate); + final max = _getDefDate(maxDate, 6); + final months = _monthsBetween(min, max); + final data = >{}; + for (var i = 0; i < months.length; i++) { + data[months[i]] = _getDaysInMonth(months[i], min, max); + final hasSelected = data[months[i]]?.isContains((item) => + item?.typeNotifier.value == DateSelectType.selected || item?.typeNotifier.value == DateSelectType.start); + if (scrollToIndex == 0 && hasSelected == true) { + scrollToIndex = i; + } + } + _scrollToItem(itemKey, scrollController, scrollToIndex); + return ListView.separated( + padding: EdgeInsets.all(bodyPadding), + controller: scrollController, + itemCount: data.keys.length, + itemBuilder: (context, index) { + final key = index == 0 ? itemKey : null; + final monthDate = data.keys.elementAt(index); + final monthYear = monthDate.year.toString() + context.resource.year; + final monthMonth = monthNames[monthDate.month - 1]; + final monthDateText = displayFormat.replaceFirst('year', monthYear).replaceFirst('month', monthMonth); + final monthData = data[monthDate]!; + return Column( + key: key, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TDText(monthDateText, style: monthTitleStyle), + ...List.generate( + (monthData.length / 7).ceil(), + (rowIndex) => [ + SizedBox(height: verticalGap), + Row( + children: List.generate( + 7, + (colIndex) => [ + if (colIndex != 0) SizedBox(width: verticalGap / 2), + Expanded( + child: builder( + monthData[rowIndex * 7 + colIndex], + monthData, + data, + rowIndex, + colIndex, + ), + ), + ], + ).expand((element) => element).toList(), + ), + ], + ).expand((element) => element).toList(), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(height: bodyPadding); + }, + ); + } + + void _scrollToItem(GlobalKey itemKey, ScrollController scrollController, int index) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final height = (itemKey.currentContext?.findRenderObject() as RenderBox?)?.size.height ?? 0; + scrollController.jumpTo(height * index + index * bodyPadding); + }); + } + + DateTime _getDefDate(int? date, [int? addMonth]) { + final now = date == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(date); + if (addMonth == null) { + return DateTime(now.year, now.month, now.day); + } + final month = now.month + addMonth; + return DateTime(now.year, date == null ? month : now.month, now.day); + } + + List _monthsBetween(DateTime min, DateTime max) { + final months = []; + var current = DateTime(min.year, min.month); + while (current.compareTo(max) <= 0) { + months.add(current); + current = DateTime(current.year, current.month + 1); + } + return months; + } + + List _getDaysInMonth(DateTime date, DateTime min, DateTime max) { + final year = date.year; + final month = date.month; + var dayOneWeek = DateTime(year, month).weekday; + dayOneWeek = dayOneWeek == 7 ? 0 : dayOneWeek; + var preOffset = dayOneWeek - firstDayOfWeek; + preOffset = preOffset < 0 ? preOffset + 7 : preOffset; + final daysInMonth = List.generate(preOffset, (index) => null); + final daysInMonthCount = DateTime(year, month + 1, 0).day; // 获取下个月的第一天的前一天,即当前月的最后一天 + for (var day = 1; day <= daysInMonthCount; day++) { + final date = DateTime(year, month, day); + var selectType = DateSelectType.empty; + if (date.compareTo(min) == -1 || date.compareTo(max) == 1) { + selectType = DateSelectType.disabled; + } else if (type == CalendarType.single && (value?.length ?? 0) >= 1) { + if (date.compareTo(value![0]) == 0) { + selectType = DateSelectType.selected; + } + } else if (type == CalendarType.multiple && value != null) { + if (value!.isContains((e) => date.compareTo(e) == 0)) { + selectType = DateSelectType.selected; + } + } else if (type == CalendarType.range && (value?.length ?? 0) >= 1) { + final end = (value?.length ?? 0) > 1 ? value![1] : null; + if (date.compareTo(value![0]) == 0) { + selectType = DateSelectType.start; + } + if (end != null && value![0].compareTo(end) < 0) { + if (date.compareTo(end) == 0) { + selectType = DateSelectType.end; + } + if (date.compareTo(value![0]) == 1 && date.compareTo(end) == -1) { + selectType = DateSelectType.centre; + } + } + } + daysInMonth.add(TDate( + date: date, + typeNotifier: DateSelectTypeNotifier(selectType), + isLastDayOfMonth: daysInMonthCount == day, + )); + } + var sufOffset = 7 - daysInMonth.length % 7; + sufOffset = sufOffset == 7 ? 0 : sufOffset; + List.generate(sufOffset, (index) => daysInMonth.add(null)); + return daysInMonth; + } +} diff --git a/tdesign-component/lib/src/components/calendar/td_calendar_cell.dart b/tdesign-component/lib/src/components/calendar/td_calendar_cell.dart new file mode 100644 index 000000000..30271b4d5 --- /dev/null +++ b/tdesign-component/lib/src/components/calendar/td_calendar_cell.dart @@ -0,0 +1,304 @@ +import 'package:flutter/material.dart'; +import '../../../tdesign_flutter.dart'; +import '../../util/iterable_ext.dart'; +import '../../util/list_ext.dart'; + +class TDCalendarCell extends StatefulWidget { + const TDCalendarCell({ + Key? key, + this.tdate, + this.format, + required this.type, + this.onCellClick, + this.onCellLongPress, + this.onChange, + required this.height, + required this.data, + required this.padding, + required this.rowIndex, + required this.colIndex, + required this.dateList, + }) : super(key: key); + + final TDate? tdate; + final CalendarFormat? format; + final CalendarType type; + final void Function(int value, DateSelectType type, TDate tdate)? onCellClick; + final void Function(int value, DateSelectType type, TDate tdate)? onCellLongPress; + final void Function(List value)? onChange; + final double height; + final Map> data; + final double padding; + final int rowIndex; + final int colIndex; + final List dateList; + + @override + _TDCalendarCellState createState() => _TDCalendarCellState(); +} + +class _TDCalendarCellState extends State { + late List list; + var isToday = false; + var positionOffset = 0; + @override + void initState() { + super.initState(); + list = widget.data.values.expand((element) => element).toList(); + isToday = _isToday(); + widget.tdate?.typeNotifier.addListener(_cellTypeChange); + } + + @override + void didUpdateWidget(TDCalendarCell oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.data != oldWidget.data) { + list = widget.data.values.expand((element) => element).toList(); + } + if (widget.tdate != oldWidget.tdate) { + oldWidget.tdate?.typeNotifier.removeListener(_cellTypeChange); + widget.tdate?.typeNotifier.addListener(_cellTypeChange); + } + } + + @override + void dispose() { + widget.tdate?.typeNotifier.removeListener(_cellTypeChange); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.tdate == null) { + return const SizedBox.shrink(); + } + final tdate = widget.format?.call(widget.tdate) ?? widget.tdate!; + final cellStyle = TDCalendarStyle.cellStyle(context, widget.tdate!._type); + final decoration = tdate.decoration ?? cellStyle.cellDecoration; + final positionColor = _getColor(cellStyle, decoration); + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: _cellTap, + onLongPress: () { + final selectType = widget.tdate!._type; + final curDate = widget.tdate!._milliseconds; + widget.onCellLongPress?.call(curDate, selectType, widget.tdate!); + }, + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + height: widget.height, + decoration: decoration, + padding: EdgeInsets.all(widget.padding), + child: Column( + children: [ + Expanded( + flex: 2, + child: tdate.prefixWidget ?? + TDText( + tdate.prefix ?? '', + style: tdate.prefixStyle ?? cellStyle.cellPrefixStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Expanded( + flex: 3, + child: Center( + child: TDText( + forceVerticalCenter: true, + widget.tdate!.date.day.toString(), + style: (isToday ? cellStyle.todayStyle : null) ?? tdate.style ?? cellStyle.cellStyle, + ), + ), + ), + Expanded( + flex: 2, + child: tdate.suffixWidget ?? + TDText( + tdate.suffix ?? '', + style: tdate.suffixStyle ?? cellStyle.cellSuffixStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + if (widget.colIndex < 6) + Positioned( + right: -widget.padding - positionOffset, + child: Container( + width: widget.padding + 2 * positionOffset, + height: widget.height, + color: positionColor, + ), + ), + ], + ), + ); + } + + void _cellTap() { + final selectType = widget.tdate!._type; + final curDate = widget.tdate!._milliseconds; + if (selectType == DateSelectType.disabled) { + widget.onCellClick?.call(curDate, selectType, widget.tdate!); + return; + } + switch (widget.type) { + case CalendarType.single: + final date = list.find((item) => item?._type == DateSelectType.selected); + date?._setType(DateSelectType.empty); + widget.tdate!._setType(DateSelectType.selected); + if (date?._milliseconds != curDate) { + widget.onChange?.call([curDate]); + } + break; + case CalendarType.multiple: + final date = list.where((item) => item?._type == DateSelectType.selected).toList(); + final value = date.map((item) => item!._milliseconds).toList(); + if (date.find((item) => item?._milliseconds == curDate) != null) { + widget.tdate!._setType(DateSelectType.empty); + value.remove(curDate); + } else { + widget.tdate!._setType(DateSelectType.selected); + value.add(curDate); + } + widget.onChange?.call(value); + break; + case CalendarType.range: + final start = list.find((item) => item?._type == DateSelectType.start); + final end = list.find((item) => item?._type == DateSelectType.end); + final startTimes = start?._milliseconds; + if ((start == null && end == null) || + (start != null && end != null) || + (start != null && end == null && startTimes! >= curDate)) { + start?._setType(DateSelectType.empty); + end?._setType(DateSelectType.empty); + final centres = list.where((item) => item?._type == DateSelectType.centre).toList(); + centres.forEach((item) => item!._setType(DateSelectType.empty)); + widget.tdate!._setType(DateSelectType.start); + widget.onChange?.call([curDate]); + } else if (start != null && end == null && startTimes! < curDate) { + start._setType(DateSelectType.start); + widget.tdate!._setType(DateSelectType.end); + var startIndex = list.indexOf(start) + 1; + while (list[startIndex] == null || list[startIndex]!._milliseconds < curDate) { + list[startIndex]?._setType(DateSelectType.centre); + startIndex++; + } + widget.onChange?.call([startTimes, curDate]); + } + break; + } + widget.onCellClick?.call(curDate, widget.tdate!._type, widget.tdate!); + } + + void _cellTypeChange() { + setState(() {}); + } + + Color? _getColor(TDCalendarStyle cellStyle, BoxDecoration? decoration) { + positionOffset = 0; + final next = _nextDay(); + if (widget.tdate?._type == DateSelectType.start) { + if (widget.tdate?.isLastDayOfMonth == true) { + return null; + } + if (next?._type == DateSelectType.end) { + positionOffset = 1; + return decoration?.color; + } + if (next?._type == DateSelectType.centre) { + return cellStyle.centreColor; + } + } + if (widget.tdate?._type == DateSelectType.centre) { + return cellStyle.centreColor; + } + return null; + } + + TDate? _nextDay([int num = 1]) { + final index = widget.rowIndex * 7 + widget.colIndex + num; + final date = widget.dateList.getOrNull(index); + return date; + } + + bool _isToday() { + final today = DateTime.now(); + return widget.tdate?._milliseconds == DateTime(today.year, today.month, today.day).millisecondsSinceEpoch; + } +} + +/// 时间对象 +class TDate { + TDate({ + required this.date, + required this.typeNotifier, + this.prefix, + this.prefixStyle, + this.prefixWidget, + this.suffix, + this.suffixStyle, + this.suffixWidget, + this.style, + this.decoration, + required this.isLastDayOfMonth, + }); + + /// 时间对象 + final DateTime date; + + /// 日期类型 + final DateSelectTypeNotifier typeNotifier; + + /// 日期前面的字符串 + String? prefix; + + /// 日期前面的字符串的样式 + TextStyle? prefixStyle; + + /// 日期前面的组件,优先级高于[prefix] + Widget? prefixWidget; + + /// 日期后面的字符串 + String? suffix; + + /// 日期后面的字符串的样式 + TextStyle? suffixStyle; + + /// 日期后面的组件,优先级高于[suffix] + Widget? suffixWidget; + + /// 日期样式 + TextStyle? style; + + /// 日期Decoration + BoxDecoration? decoration; + + /// 是否是当月最后一天 + final bool isLastDayOfMonth; + + int get _milliseconds => DateTime(date.year, date.month, date.day).millisecondsSinceEpoch; + + DateSelectType get _type => typeNotifier.value; + + void _setType(DateSelectType type) { + typeNotifier.setType(type); + } +} + +class DateSelectTypeNotifier extends ChangeNotifier { + DateSelectType value = DateSelectType.empty; + DateSelectTypeNotifier(DateSelectType selectType) { + value = selectType; + } + + void setType(DateSelectType type) { + value = type; + notifyListeners(); + } +} diff --git a/tdesign-component/lib/src/components/calendar/td_calendar_header.dart b/tdesign-component/lib/src/components/calendar/td_calendar_header.dart new file mode 100644 index 000000000..589c734c0 --- /dev/null +++ b/tdesign-component/lib/src/components/calendar/td_calendar_header.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import '../../../tdesign_flutter.dart'; + +class TDCalendarHeader extends StatelessWidget { + const TDCalendarHeader({ + Key? key, + required this.firstDayOfWeek, + required this.weekdayGap, + required this.padding, + this.weekdayStyle, + required this.weekdayHeight, + this.title, + this.titleStyle, + this.titleWidget, + this.titleMaxLine, + this.titleOverflow, + this.closeBtn = true, + this.closeColor, + this.onClose, + this.onClick, + required this.weekdayNames, + }) : super(key: key); + + final int firstDayOfWeek; + final double weekdayGap; + final double padding; + final TextStyle? weekdayStyle; + final double weekdayHeight; + final String? title; + final TextStyle? titleStyle; + final Widget? titleWidget; + final int? titleMaxLine; + final TextOverflow? titleOverflow; + final bool closeBtn; + final Color? closeColor; + final VoidCallback? onClose; + final List weekdayNames; + final void Function(int index, String week)? onClick; + + List _getWeeks(BuildContext context) { + final ans = []; + var i = firstDayOfWeek % 7; + while (ans.length < 7) { + ans.add(weekdayNames[i]); + i = (i + 1) % 7; + } + return ans; + } + + @override + Widget build(BuildContext context) { + final list = _getWeeks(context); + return Container( + padding: EdgeInsets.fromLTRB(padding, 0, padding, 0), + child: Column( + children: [ + if (title?.isNotEmpty == true || titleWidget != null || closeBtn) + Container( + padding: EdgeInsets.fromLTRB(0, padding, 0, padding), + child: Row( + children: [ + if (closeBtn) const SizedBox(width: 24), + Expanded( + child: Center( + child: titleWidget ?? + TDText( + title, + style: titleStyle, + maxLines: titleMaxLine, + overflow: TextOverflow.ellipsis, + ), + ), + ), + if (closeBtn) + SizedBox( + width: 24, + child: GestureDetector( + child: Icon(TDIcons.close, color: closeColor), + onTap: () { + onClose?.call(); + }, + ), + ), + ], + ), + ), + Row( + children: List.generate(list.length, (index) { + return [ + if (index != 0) SizedBox(width: weekdayGap), + Expanded( + child: GestureDetector( + onTap: () { + onClick?.call(index, list[index]); + }, + child: SizedBox( + height: weekdayHeight, + child: Center( + child: TDText( + list[index], + style: weekdayStyle, + ), + ), + ), + ), + ), + ]; + }).expand((element) => element).toList(), + ), + ], + ), + ); + } +} diff --git a/tdesign-component/lib/src/components/calendar/td_calendar_popup.dart b/tdesign-component/lib/src/components/calendar/td_calendar_popup.dart new file mode 100644 index 000000000..530cca77d --- /dev/null +++ b/tdesign-component/lib/src/components/calendar/td_calendar_popup.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import '../../../tdesign_flutter.dart'; + +typedef CalendarBuilder = TDCalendar Function(BuildContext context); + +enum CalendarTrigger { closeBtn, confirmBtn, overlay } + +/// 单元格组件popup模式 +class TDCalendarPopup { + TDCalendarPopup( + this.context, { + this.top, + this.autoClose = true, + this.confirmBtn, + this.visible, + this.onClose, + this.onConfirm, + this.builder, + this.child, + }) { + if (builder == null && child == null) { + throw FlutterError('[TDCalendarPopup] builder or child must be not null'); + } + if (visible == true) { + show(); + } + } + + /// 上下文 + final BuildContext context; + + /// 距离顶部的距离 + final double? top; + + /// 自动关闭;在点击关闭按钮、确认按钮、遮罩层时自动关闭 + final bool? autoClose; + + /// 自定义确认按钮 + final Widget? confirmBtn; + + /// 默认是否显示日历 + final bool? visible; + + /// 关闭时触发 + final VoidCallback? onClose; + + /// 控件构建器,优先级高于[child] + final CalendarBuilder? builder; + + /// 日历控件 + final TDCalendar? child; + + /// 点击确认按钮时触发 + final void Function(List value)? onConfirm; + + static TDSlidePopupRoute? _calendarPopup; + + /// 当前选中值 + final ValueNotifier> _selected = ValueNotifier>([]); + + bool get _autoClose => autoClose ?? true; + + /// 当前选中值 + List get selected => _selected.value; + + /// 打开日历 + void show() { + if (_calendarPopup != null) { + return; + } + _calendarPopup = TDSlidePopupRoute( + isDismissible: false, + slideTransitionFrom: SlideTransitionFrom.bottom, + modalTop: top, + barrierClick: () { + if (_autoClose) { + close(); + } + }, + builder: (context) { + final childWidget = builder?.call(context) ?? child; + return TDCalendarInherited( + selected: _selected, + usePopup: true, + confirmBtn: confirmBtn, + onClose: _onClose, + onConfirm: _onConfirm, + child: childWidget!, + ); + }, + ); + Navigator.of(context).push(_calendarPopup!).then((_) { + _deleteRouter(); + }); + } + + void _onClose() { + if (_autoClose) { + close(); + } + } + + void _onConfirm() { + onConfirm?.call(_selected.value); + if (_autoClose) { + close(); + } + } + + /// 关闭日历 + void close() { + if (_calendarPopup != null) { + Navigator.of(context).pop(); + // _deleteRouter(); + } + } + + void _deleteRouter() { + _calendarPopup = null; + onClose?.call(); + } +} + +class TDCalendarInherited extends InheritedWidget { + const TDCalendarInherited({ + required Widget child, + this.onClose, + required this.selected, + this.usePopup = true, + this.onConfirm, + this.confirmBtn, + Key? key, + }) : super(child: child, key: key); + + final VoidCallback? onClose; + final ValueNotifier> selected; + final bool? usePopup; + final VoidCallback? onConfirm; + final Widget? confirmBtn; + + @override + bool updateShouldNotify(covariant TDCalendarInherited oldWidget) { + return false; + } + + static TDCalendarInherited? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } +} diff --git a/tdesign-component/lib/src/components/calendar/td_calendar_style.dart b/tdesign-component/lib/src/components/calendar/td_calendar_style.dart new file mode 100644 index 000000000..ddf243fde --- /dev/null +++ b/tdesign-component/lib/src/components/calendar/td_calendar_style.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import '../../../tdesign_flutter.dart'; + +/// 日历组件样式 +class TDCalendarStyle { + TDCalendarStyle({ + this.decoration, + this.titleStyle, + this.titleMaxLine, + this.titleCloseColor, + this.weekdayStyle, + this.monthTitleStyle, + this.cellStyle, + this.centreColor, + this.cellDecoration, + this.cellPrefixStyle, + this.cellSuffixStyle, + }); + + BoxDecoration? decoration; + + /// header区域 [TDCalendar.title]的样式 + TextStyle? titleStyle; + + /// header区域 [TDCalendar.title]的行数 + int? titleMaxLine; + + /// header区域 关闭图标的颜色 + Color? titleCloseColor; + + /// header区域 周 文字样式 + TextStyle? weekdayStyle; + + /// body区域 年月文字样式 + TextStyle? monthTitleStyle; + + /// 日期样式 + TextStyle? cellStyle; + + /// 当天日期样式 + TextStyle? todayStyle; + + /// 日期decoration + BoxDecoration? cellDecoration; + + /// 日期范围内背景样式 + Color? centreColor; + + /// 日期前面的字符串的样式 + TextStyle? cellPrefixStyle; + + /// 日期后面的字符串的样式 + TextStyle? cellSuffixStyle; + + /// 生成默认样式 + TDCalendarStyle.generateStyle(BuildContext context) { + decoration = BoxDecoration( + color: TDTheme.of(context).whiteColor1, + borderRadius: BorderRadius.vertical( + top: Radius.circular(TDTheme.of(context).radiusExtraLarge), + ), + ); + titleStyle = TextStyle( + fontSize: TDTheme.of(context).fontTitleLarge?.size, + fontWeight: TDTheme.of(context).fontTitleLarge?.fontWeight, + color: TDTheme.of(context).fontGyColor1, + ); + titleMaxLine = 1; + titleCloseColor = titleStyle?.color; + weekdayStyle = TextStyle( + fontSize: TDTheme.of(context).fontTitleSmall?.size, + color: TDTheme.of(context).fontGyColor2, + ); + monthTitleStyle = TextStyle( + fontSize: TDTheme.of(context).fontMarkMedium?.size, + fontWeight: TDTheme.of(context).fontMarkMedium?.fontWeight, + color: TDTheme.of(context).fontGyColor1, + ); + } + + /// 日期样式 + TDCalendarStyle.cellStyle(BuildContext context, DateSelectType? type) { + final radius6 = TDTheme.of(context).radiusDefault; + final defStyle = TextStyle( + fontSize: TDTheme.of(context).fontTitleMedium?.size, + height: TDTheme.of(context).fontTitleMedium?.height, + fontWeight: TDTheme.of(context).fontTitleMedium?.fontWeight, + ); + final prefixStyle = TextStyle( + fontSize: TDTheme.of(context).fontBodyExtraSmall?.size, + height: TDTheme.of(context).fontBodyExtraSmall?.height, + fontWeight: FontWeight.w400, + ); + centreColor = TDTheme.of(context).brandColor1; + switch (type) { + case DateSelectType.empty: + cellStyle = defStyle.copyWith(color: TDTheme.of(context).fontGyColor1); + todayStyle = defStyle.copyWith(color: TDTheme.of(context).brandColor7); + cellPrefixStyle = prefixStyle.copyWith(color: TDTheme.of(context).errorColor6); + cellSuffixStyle = prefixStyle.copyWith(color: TDTheme.of(context).fontGyColor3); + cellDecoration = null; + break; + case DateSelectType.disabled: + cellStyle = defStyle.copyWith(color: TDTheme.of(context).fontGyColor4); + todayStyle = defStyle.copyWith(color: TDTheme.of(context).brandColor3); + cellPrefixStyle = prefixStyle.copyWith(color: TDTheme.of(context).errorColor3); + cellSuffixStyle = prefixStyle.copyWith(color: TDTheme.of(context).fontGyColor4); + cellDecoration = null; + break; + case DateSelectType.selected: + cellStyle = defStyle.copyWith(color: TDTheme.of(context).fontWhColor1); + cellPrefixStyle = prefixStyle.copyWith(color: TDTheme.of(context).fontWhColor1); + cellSuffixStyle = prefixStyle.copyWith(color: TDTheme.of(context).fontWhColor1); + cellDecoration = BoxDecoration( + borderRadius: BorderRadius.circular(radius6), + color: TDTheme.of(context).brandColor7, + ); + break; + case DateSelectType.centre: + cellStyle = defStyle.copyWith(color: TDTheme.of(context).fontGyColor1); + cellPrefixStyle = prefixStyle.copyWith(color: TDTheme.of(context).errorColor6); + cellSuffixStyle = prefixStyle.copyWith(color: TDTheme.of(context).fontGyColor3); + cellDecoration = BoxDecoration( + color: centreColor, + ); + break; + case DateSelectType.start: + cellStyle = defStyle.copyWith(color: TDTheme.of(context).fontWhColor1); + cellPrefixStyle = prefixStyle.copyWith(color: TDTheme.of(context).fontWhColor1); + cellSuffixStyle = prefixStyle.copyWith(color: TDTheme.of(context).fontWhColor1); + cellDecoration = BoxDecoration( + color: TDTheme.of(context).brandColor7, + borderRadius: BorderRadius.horizontal(left: Radius.circular(radius6)), + ); + break; + case DateSelectType.end: + cellStyle = defStyle.copyWith(color: TDTheme.of(context).fontWhColor1); + cellPrefixStyle = prefixStyle.copyWith(color: TDTheme.of(context).fontWhColor1); + cellSuffixStyle = prefixStyle.copyWith(color: TDTheme.of(context).fontWhColor1); + cellDecoration = BoxDecoration( + color: TDTheme.of(context).brandColor7, + borderRadius: BorderRadius.horizontal(right: Radius.circular(radius6)), + ); + break; + default: + break; + } + } +} diff --git a/tdesign-component/lib/src/components/cascader/td_cascader.dart b/tdesign-component/lib/src/components/cascader/td_cascader.dart index 67a82e592..ee35a94ff 100644 --- a/tdesign-component/lib/src/components/cascader/td_cascader.dart +++ b/tdesign-component/lib/src/components/cascader/td_cascader.dart @@ -15,6 +15,7 @@ class TDCascader { double cascaderHeight = 500, String? initialData, String? closeText, + bool isLetterSort=false, List? subTitles, Function? onClose}) { showModalBottomSheet( @@ -32,6 +33,7 @@ class TDCascader { onChange: onChange, closeText: closeText, theme: theme, + isLetterSort:isLetterSort, subTitles: subTitles); }); } diff --git a/tdesign-component/lib/src/components/cascader/td_multi_cascader.dart b/tdesign-component/lib/src/components/cascader/td_multi_cascader.dart index 5d77e19f7..f793f95ae 100644 --- a/tdesign-component/lib/src/components/cascader/td_multi_cascader.dart +++ b/tdesign-component/lib/src/components/cascader/td_multi_cascader.dart @@ -34,6 +34,9 @@ class TDMultiCascader extends StatefulWidget { /// 顶部圆角 final double? topRadius; + /// 是否开启字母排序 + final bool isLetterSort; + /// 关闭按钮文本 final String? closeText; @@ -55,6 +58,7 @@ class TDMultiCascader extends StatefulWidget { this.backgroundColor, this.topRadius, this.closeText, + this.isLetterSort = false, this.onClose, required this.onChange}); @@ -63,7 +67,6 @@ class TDMultiCascader extends StatefulWidget { } class _TDMultiCascaderState extends State with TickerProviderStateMixin { - List _tabListData = []; /// 当前tab选中的值 @@ -90,17 +93,19 @@ class _TDMultiCascaderState extends State with TickerProviderSt MultiCascaderListModel item = MultiCascaderListModel( label: widget.data[index]['label'], value: widget.data[index]['value'], - segmentValue:widget.data[index]['segmentValue'], + segmentValue: widget.data[index]['segmentValue'], level: 0, ); _listData.add(item); + if (widget.data[index]['children'] != null && widget.data[index]['children'].length > 0) { _buildRecursiveList(1, widget.data[index]['value'], widget.data[index]['children']); } }); - _listDataSegmenter(); + if (widget.isLetterSort) { + _listDataSegmenter(); + } _selectListData = _listData.where((element) => element.level == 0).toList(); - _tabListData.add(MultiCascaderListModel( label: '选择选项', )); @@ -108,7 +113,7 @@ class _TDMultiCascaderState extends State with TickerProviderSt _tabListData.clear(); _initLocation(widget.initialData!); _currentTabIndex = _tabListData.length - 1; - _level=_currentTabIndex; + _level = _currentTabIndex; _tabListData = _tabListData.reversed.toList(); _selectTabValue = widget.initialData; _selectListData = @@ -131,11 +136,7 @@ class _TDMultiCascaderState extends State with TickerProviderSt ), child: Column( mainAxisSize: MainAxisSize.min, - children: [ - _buildTitle(context), - _buildTabThemeBox(context), - Expanded(child: _buildContentBox(context)) - ], + children: [_buildTitle(context), _buildTabThemeBox(context), Expanded(child: _buildContentBox(context))], ), ); } @@ -169,11 +170,11 @@ class _TDMultiCascaderState extends State with TickerProviderSt } } - void _listDataSegmenter(){ - _listData.sort((a,b){ - if(a.segmentValue==null||b.segmentValue==null){ - return 0; - }else{ + void _listDataSegmenter() { + _listData.sort((a, b) { + if (a.segmentValue == null || b.segmentValue == null) { + return 0; + } else { return a.segmentValue!.toLowerCase().compareTo(b.segmentValue!.toLowerCase()); } }); @@ -185,7 +186,7 @@ class _TDMultiCascaderState extends State with TickerProviderSt label: data[index]['label'], value: data[index]['value'], parentValue: parentValue, - segmentValue:data[index]['segmentValue'], + segmentValue: data[index]['segmentValue'], level: depth, ); _listData.add(item); @@ -224,14 +225,19 @@ class _TDMultiCascaderState extends State with TickerProviderSt child: Container( height: 58, alignment: Alignment.center, - child:Padding( + child: Padding( padding: const EdgeInsets.only(left: 2, right: 16), - child: widget.closeText==null ? Icon( - TDIcons.close, - color: TDTheme.of(context).fontGyColor1, - ):TDText(widget.closeText,style:TextStyle( - fontSize: TDTheme.of(context).fontTitleMedium!.size, - color: TDTheme.of(context).fontGyColor1),), + child: widget.closeText == null + ? Icon( + TDIcons.close, + color: TDTheme.of(context).fontGyColor1, + ) + : TDText( + widget.closeText, + style: TextStyle( + fontSize: TDTheme.of(context).fontTitleMedium!.size, + color: TDTheme.of(context).fontGyColor1), + ), ), ))), ], @@ -247,7 +253,7 @@ class _TDMultiCascaderState extends State with TickerProviderSt Widget _buildStepBox(BuildContext context) { var maxWidth = MediaQuery.of(context).size.width; return Container( - decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Color.fromRGBO(0, 0, 0, 0.1),width: 0.5))), + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Color.fromRGBO(0, 0, 0, 0.1), width: 0.5))), padding: EdgeInsets.only(bottom: 11), width: maxWidth, child: ListView( @@ -278,7 +284,7 @@ class _TDMultiCascaderState extends State with TickerProviderSt style: TextStyle( fontSize: 14, color: _currentTabIndex == index ? TDTheme.of(context).brandNormalColor : Colors.black), - fontWeight: _currentTabIndex == index?FontWeight.w600:FontWeight.w400, + fontWeight: _currentTabIndex == index ? FontWeight.w600 : FontWeight.w400, ), ), Padding( @@ -298,7 +304,7 @@ class _TDMultiCascaderState extends State with TickerProviderSt var maxWidth = MediaQuery.of(context).size.width; return Container( height: 48, - decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Color.fromRGBO(0, 0, 0, 0.1),width: 0.5))), + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Color.fromRGBO(0, 0, 0, 0.1), width: 0.5))), width: maxWidth, child: TDCustomTab( tabs: List.generate(_tabListData.length, (index) { @@ -317,92 +323,101 @@ class _TDMultiCascaderState extends State with TickerProviderSt return Container( width: maxWidth, padding: EdgeInsets.only(left: 16, right: 16), - child:Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if(widget.subTitles!=null) - Container( - height: 50, - padding: EdgeInsets.only(top: 20,), - child:TDText(widget.subTitles![_level],style: TextStyle(color: Color.fromRGBO(0, 0, 0, 0.4)),font: TDTheme.of(context).fontTitleSmall,) //, - ), - Expanded(child: PageView( - scrollDirection: Axis.horizontal, - reverse: false, - controller: PageController(initialPage: 1, keepPage: false), - children: List.generate(1, (index) { - return ListView.builder( - controller: _scrollListController, - itemCount: _selectListData.length, - itemBuilder: (context, index) { - MultiCascaderListModel item = _selectListData[index]; - MultiCascaderListModel preItem =index==0?MultiCascaderListModel():_selectListData[index-1]; - return GestureDetector( - onTap: () { - int level = 0; - if (_tabListData.length > 2 && _currentTabIndex == 0) { - _tabListData.clear(); - _tabListData.add(MultiCascaderListModel( - label: '选择选项', - )); - } - if (item.level != null) { - level = item.level!; - } - - if(widget.subTitles!=null&&widget.subTitles!.length-1>_level){ - _level=level+1; - } - List isList = _tabListData.where((element) => element.level == item.level).toList(); - if (isList.isNotEmpty) { - _tabListData.removeAt(level); - } - setState(() { - _tabListData.insert(level, item); - _selectTabValue = item.value; - //下一级查询 - _getChildrenListData(level + 1, item.value!); - }); - }, - child: Container( - height: 56, - decoration: BoxDecoration(border: Border.all(color: Colors.transparent)), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if(item.segmentValue!=null) - SizedBox( - width:32, - child:item.segmentValue!=preItem.segmentValue?TDText( - '${item.segmentValue}', - font: Font(size: 16, lineHeight: 24), - ):null, - ), - TDText( - '${item.label}', - font: Font(size: 16, lineHeight: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.subTitles != null) + Container( + height: 50, + padding: EdgeInsets.only( + top: 20, + ), + child: TDText( + widget.subTitles![_level], + style: TextStyle(color: Color.fromRGBO(0, 0, 0, 0.4)), + font: TDTheme.of(context).fontTitleSmall, + ) //, + ), + Expanded( + child: PageView( + scrollDirection: Axis.horizontal, + reverse: false, + controller: PageController(initialPage: 1, keepPage: false), + children: List.generate(1, (index) { + return ListView.builder( + controller: _scrollListController, + itemCount: _selectListData.length, + itemBuilder: (context, index) { + MultiCascaderListModel item = _selectListData[index]; + MultiCascaderListModel preItem = index == 0 ? MultiCascaderListModel() : _selectListData[index - 1]; + return GestureDetector( + onTap: () { + int level = 0; + if (_tabListData.length > 2 && _currentTabIndex == 0) { + _tabListData.clear(); + _tabListData.add(MultiCascaderListModel( + label: '选择选项', + )); + } + if (item.level != null) { + level = item.level!; + } + + if (widget.subTitles != null && widget.subTitles!.length - 1 > _level) { + _level = level + 1; + } + List isList = _tabListData.where((element) => element.level == item.level).toList(); + if (isList.isNotEmpty) { + _tabListData.removeAt(level); + } + setState(() { + _tabListData.insert(level, item); + _selectTabValue = item.value; + //下一级查询 + _getChildrenListData(level + 1, item.value!); + }); + }, + child: Container( + height: 56, + decoration: BoxDecoration(border: Border.all(color: Colors.transparent)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (item.segmentValue != null) + SizedBox( + width: 32, + child: item.segmentValue != preItem.segmentValue + ? TDText( + '${item.segmentValue}', + font: Font(size: 16, lineHeight: 24), + ) + : null, ), - ], - ), + TDText( + '${item.label}', + font: Font(size: 16, lineHeight: 24), + ), + ], ), - if (_selectTabValue == item.value) - Icon( - TDIcons.check, - color: TDTheme.of(context).brandNormalColor, - ) - ], - )), - ); - }, - ); - }), - )) - ], + ), + if (_selectTabValue == item.value) + Icon( + TDIcons.check, + color: TDTheme.of(context).brandNormalColor, + ) + ], + )), + ); + }, + ); + }), + )) + ], )); } @@ -415,10 +430,10 @@ class _TDMultiCascaderState extends State with TickerProviderSt if (index < _tabListData.length - 1) { _getFindListData(level: tabItem.level!, value: tabItem.value); } else { - int cruIndex=index>0?index-1:index; + int cruIndex = index > 0 ? index - 1 : index; _getFindListData(level: index, parentValue: _tabListData[cruIndex].value); } - _level=index; + _level = index; setState(() {}); } @@ -428,8 +443,7 @@ class _TDMultiCascaderState extends State with TickerProviderSt //判断下级是否存在 if (selectLevelData.isNotEmpty) { //获取下级数据 - var childList = - selectLevelData.where((element) => element.parentValue == value).toList(); + var childList = selectLevelData.where((element) => element.parentValue == value).toList(); _selectListData = childList; _currentTabIndex += 1; } else { @@ -457,7 +471,7 @@ class _TDMultiCascaderState extends State with TickerProviderSt } /// 定位选项在列表中位置 - void _scrollToListIndex(int index) async{ + void _scrollToListIndex(int index) async { // 计算列表中特定索引的位置 double scrollTo = index * 56.0; // 每个列表项的高度是56.0 _scrollListController.animateTo( @@ -545,5 +559,5 @@ class MultiCascaderListModel { String? segmentValue; int? level; - MultiCascaderListModel({this.label, this.value, this.parentValue, this.level,this.segmentValue}); + MultiCascaderListModel({this.label, this.value, this.parentValue, this.level, this.segmentValue}); } diff --git a/tdesign-component/lib/src/components/cell/td_cell.dart b/tdesign-component/lib/src/components/cell/td_cell.dart index 32f4c279e..916a2bd01 100644 --- a/tdesign-component/lib/src/components/cell/td_cell.dart +++ b/tdesign-component/lib/src/components/cell/td_cell.dart @@ -144,7 +144,7 @@ class _TDCellState extends State { }, child: Container( color: _status == 'default' ? style.backgroundColor : style.clickBackgroundColor, - padding: EdgeInsets.all(TDTheme.of(context).spacer16), + padding: style.padding, child: Row( crossAxisAlignment: crossAxisAlignment, children: [ diff --git a/tdesign-component/lib/src/components/cell/td_cell_style.dart b/tdesign-component/lib/src/components/cell/td_cell_style.dart index e742b5392..309b16c0e 100644 --- a/tdesign-component/lib/src/components/cell/td_cell_style.dart +++ b/tdesign-component/lib/src/components/cell/td_cell_style.dart @@ -14,6 +14,7 @@ class TDCellStyle { this.borderedColor, this.groupBorderedColor, this.backgroundColor, + this.padding }); /// 左侧图标颜色 @@ -52,6 +53,9 @@ class TDCellStyle { /// 单元组标题文字样式 TextStyle? groupTitleStyle; + /// 单元格内边距 + EdgeInsets? padding; + /// 生成单元格默认样式 TDCellStyle.cellStyle(BuildContext context) { backgroundColor = Colors.white; @@ -82,5 +86,7 @@ class TDCellStyle { height: TDTheme.of(context).fontTitleLarge?.height ?? 26, fontWeight: TDTheme.of(context).fontTitleLarge?.fontWeight ?? FontWeight.w600, ); + + padding = EdgeInsets.all(TDTheme.of(context).spacer16); } } diff --git a/tdesign-component/lib/src/components/dialog/td_alert_dialog.dart b/tdesign-component/lib/src/components/dialog/td_alert_dialog.dart index 39b74e1a5..3d73a7b6b 100644 --- a/tdesign-component/lib/src/components/dialog/td_alert_dialog.dart +++ b/tdesign-component/lib/src/components/dialog/td_alert_dialog.dart @@ -35,6 +35,8 @@ class TDAlertDialog extends StatelessWidget { this.rightBtnAction, this.showCloseButton, TDDialogButtonStyle buttonStyle = TDDialogButtonStyle.normal, + this.padding = const EdgeInsets.fromLTRB(24, 32, 24, 0), + this.buttonWidget, }) : assert((title != null || content != null)), _vertical = false, _buttons = null, @@ -57,6 +59,8 @@ class TDAlertDialog extends StatelessWidget { this.contentColor, this.contentMaxHeight = 0, this.showCloseButton, + this.padding = const EdgeInsets.fromLTRB(24, 32, 24, 0), + this.buttonWidget, }) : _vertical = true, leftBtn = null, rightBtn = null, @@ -121,6 +125,12 @@ class TDAlertDialog extends StatelessWidget { /// [leftBtn]和[rightBtn]中的style会覆盖此配置 final TDDialogButtonStyle _buttonStyle; + /// 内容内边距 + final EdgeInsets? padding; + + /// 自定义按钮 + final Widget? buttonWidget; + @override Widget build(BuildContext context) { // 标题和内容不能同时为空 @@ -137,6 +147,7 @@ class TDAlertDialog extends StatelessWidget { content: content, contentColor: contentColor, contentMaxHeight: contentMaxHeight, + padding: padding, ), const TDDivider(height: 24, color: Colors.transparent), _vertical ? _verticalButtons(context) : _horizontalButtons(context), @@ -144,6 +155,9 @@ class TDAlertDialog extends StatelessWidget { } Widget _horizontalButtons(BuildContext context) { + if(buttonWidget != null) { + return buttonWidget!; + } final left = leftBtn ?? TDDialogButtonOptions( title: context.resource.cancel, theme: TDButtonTheme.light, action: leftBtnAction); diff --git a/tdesign-component/lib/src/components/dialog/td_confirm_dialog.dart b/tdesign-component/lib/src/components/dialog/td_confirm_dialog.dart index 77729975a..25329eb1f 100644 --- a/tdesign-component/lib/src/components/dialog/td_confirm_dialog.dart +++ b/tdesign-component/lib/src/components/dialog/td_confirm_dialog.dart @@ -30,6 +30,8 @@ class TDConfirmDialog extends StatelessWidget { this.buttonTextColor, this.buttonStyle = TDDialogButtonStyle.normal, this.showCloseButton, + this.padding = const EdgeInsets.fromLTRB(24, 32, 24, 0), + this.buttonWidget, }) : super(key: key); /// 标题 @@ -74,7 +76,16 @@ class TDConfirmDialog extends StatelessWidget { /// 右上角关闭按钮 final bool? showCloseButton; + /// 内容内边距 + final EdgeInsets? padding; + + /// 自定义按钮 + final Widget? buttonWidget; + Widget _buildButton(BuildContext context) { + if(buttonWidget != null) { + return buttonWidget!; + } if (buttonStyle == TDDialogButtonStyle.text) { return Column( mainAxisSize: MainAxisSize.min, @@ -133,6 +144,7 @@ class TDConfirmDialog extends StatelessWidget { content: content, contentColor: contentColor, contentMaxHeight: contentMaxHeight, + padding: padding, ), _buildButton(context), ])); diff --git a/tdesign-component/lib/src/components/dialog/td_dialog_widget.dart b/tdesign-component/lib/src/components/dialog/td_dialog_widget.dart index 584405f42..93b0fdecf 100644 --- a/tdesign-component/lib/src/components/dialog/td_dialog_widget.dart +++ b/tdesign-component/lib/src/components/dialog/td_dialog_widget.dart @@ -164,7 +164,7 @@ class TDDialogInfoWidget extends StatelessWidget { @override Widget build(BuildContext context) { // 标题和内容不能同时为空 - assert((title != null || content != null)); + assert((title != null || content != null || contentWidget != null)); return Container( padding: padding, child: Column( diff --git a/tdesign-component/lib/src/components/dialog/td_image_dialog.dart b/tdesign-component/lib/src/components/dialog/td_image_dialog.dart index 81d9ec4f7..e1e0c4ba3 100644 --- a/tdesign-component/lib/src/components/dialog/td_image_dialog.dart +++ b/tdesign-component/lib/src/components/dialog/td_image_dialog.dart @@ -32,6 +32,8 @@ class TDImageDialog extends StatelessWidget { this.leftBtn, this.rightBtn, this.showCloseButton, + this.padding, + this.buttonWidget, }) : super(key: key); /// 背景颜色 @@ -73,6 +75,12 @@ class TDImageDialog extends StatelessWidget { /// 显示右上角关闭按钮 final bool? showCloseButton; + /// 内容内边距 + final EdgeInsets? padding; + + /// 自定义按钮 + final Widget? buttonWidget; + Widget _buildImage(BuildContext context) { return SizedBox( width: 311, @@ -94,7 +102,7 @@ class TDImageDialog extends StatelessWidget { ), TDDialogInfoWidget( title: title, - padding: const EdgeInsets.fromLTRB(24, 24, 24, 0), + padding: padding ?? const EdgeInsets.fromLTRB(24, 24, 24, 0), titleColor: titleColor, titleAlignment: titleAlignment, contentWidget: contentWidget, @@ -109,7 +117,7 @@ class TDImageDialog extends StatelessWidget { Widget _buildMiddleImage(BuildContext context) { return Column(mainAxisSize: MainAxisSize.min, children: [ TDDialogInfoWidget( - padding: const EdgeInsets.fromLTRB(24, 24, 24, 0), + padding: padding ?? const EdgeInsets.fromLTRB(24, 24, 24, 0), title: title, titleColor: titleColor, titleAlignment: titleAlignment, @@ -161,6 +169,9 @@ class TDImageDialog extends StatelessWidget { } Widget _horizontalButtons(BuildContext context) { + if (buttonWidget != null) { + return buttonWidget!; + } final left = leftBtn ?? TDDialogButtonOptions( title: context.resource.cancel, theme: TDButtonTheme.light, action: null); diff --git a/tdesign-component/lib/src/components/dialog/td_input_dialog.dart b/tdesign-component/lib/src/components/dialog/td_input_dialog.dart index 6bad33094..074a3f789 100644 --- a/tdesign-component/lib/src/components/dialog/td_input_dialog.dart +++ b/tdesign-component/lib/src/components/dialog/td_input_dialog.dart @@ -27,6 +27,8 @@ class TDInputDialog extends StatelessWidget { this.leftBtn, this.rightBtn, this.showCloseButton, + this.padding = const EdgeInsets.fromLTRB(24, 32, 24, 0), + this.buttonWidget, }) : assert((title != null || content != null)), super(key: key); @@ -69,6 +71,12 @@ class TDInputDialog extends StatelessWidget { /// 显示右上角关闭按钮 final bool? showCloseButton; + /// 内容内边距 + final EdgeInsets? padding; + + /// 自定义按钮 + final Widget? buttonWidget; + @override Widget build(BuildContext context) { return TDDialogScaffold( @@ -87,6 +95,7 @@ class TDInputDialog extends StatelessWidget { contentWidget: contentWidget, content: content, contentColor: contentColor, + padding: padding, ), Container( height: 48, @@ -118,6 +127,9 @@ class TDInputDialog extends StatelessWidget { } Widget _horizontalButtons(BuildContext context) { + if(buttonWidget != null) { + return buttonWidget!; + } final left = leftBtn ?? TDDialogButtonOptions(title: context.resource.cancel, titleColor: const Color(0xE6000000), fontWeight: FontWeight.normal, action: null, height: 56); final right = rightBtn ?? diff --git a/tdesign-component/lib/src/components/drawer/td_drawer.dart b/tdesign-component/lib/src/components/drawer/td_drawer.dart index 5c40a9ef0..886a21fe5 100644 --- a/tdesign-component/lib/src/components/drawer/td_drawer.dart +++ b/tdesign-component/lib/src/components/drawer/td_drawer.dart @@ -32,6 +32,9 @@ class TDDrawer { this.drawerTop, this.style, this.hover = true, + this.backgroundColor, + this.bordered = true, + this.isShowLastBordered = true, }) { if (visible == true) { show(); @@ -83,6 +86,15 @@ class TDDrawer { /// 是否开启点击反馈 final bool? hover; + /// 组件背景颜色 + final Color? backgroundColor; + + /// 是否显示边框 + final bool? bordered; + + /// 是否显示最后一行分割线 + final bool? isShowLastBordered; + static TDSlidePopupRoute? _drawerRoute; void show() { @@ -112,6 +124,7 @@ class TDDrawer { title: item.title, leftIconWidget: item.icon, hover: hover, + bordered: bordered, onClick: (cell) { if (onItemClick == null) { return; @@ -124,7 +137,7 @@ class TDDrawer { .values .toList(); return Container( - color: Colors.white, + color: backgroundColor ?? Colors.white, width: width ?? 280, height: double.infinity, child: Column( @@ -135,7 +148,7 @@ class TDDrawer { titleWidget: titleWidget, style: cellStyle, scrollable: true, - isShowLastBordered: true, + isShowLastBordered: isShowLastBordered, cells: cells ?? [], ), ), @@ -158,16 +171,14 @@ class TDDrawer { @mustCallSuper void close() { if (_drawerRoute != null) { - Navigator.of(context).removeRoute(_drawerRoute!); - _deleteRouter(); + Navigator.of(context).pop(); + // _deleteRouter(); } } void _deleteRouter() { _drawerRoute = null; - if (onClose != null) { - onClose!(); - } + onClose?.call(); } } diff --git a/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_item.dart b/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_item.dart index 258e408f9..ba7afec93 100644 --- a/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_item.dart +++ b/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_item.dart @@ -28,12 +28,13 @@ int _num(List list, int? n) { return list.length + list.length % val; } -/// 下拉菜单 +/// 下拉菜单内容 class TDDropdownItem extends StatefulWidget { const TDDropdownItem({ Key? key, this.disabled = false, this.label, + this.arrowIcon, this.multiple = false, this.options = const [], this.builder, @@ -43,6 +44,8 @@ class TDDropdownItem extends StatefulWidget { this.onReset, this.minHeight, this.maxHeight, + this.tabBarWidth, + this.tabBarAlign, }) : super(key: key); /// 是否禁用 @@ -51,6 +54,9 @@ class TDDropdownItem extends StatefulWidget { /// 标题 final String? label; + /// 自定义箭头图标 + final IconData? arrowIcon; + /// 是否多选 final bool? multiple; @@ -78,6 +84,12 @@ class TDDropdownItem extends StatefulWidget { /// 内容最大高度 final double? maxHeight; + /// 该item在menu上的宽度,仅在[TDDropdownMenu.isScrollable]为true时有效 + final double? tabBarWidth; + + /// [label]和[arrowIcon]/[TDDropdownMenu.arrowIcon]的对齐方式 + final MainAxisAlignment? tabBarAlign; + static const double operateHeight = 73; double? get minContentHeight => @@ -124,54 +136,57 @@ class _TDDropdownItemState extends State { : max(popupState.maxContentHeight - TDDropdownItem.operateHeight, 0); return Column( children: [ - ConstrainedBox( - constraints: BoxConstraints(minHeight: widget.minContentHeight ?? 0.0, maxHeight: maxContentHeight), - child: SingleChildScrollView( - child: Column( - children: List.generate(groupCunck.length, (index) { - var entry = groupCunck.entries.elementAt(index); - var chunks = entry.value; - return Column( - children: [ - groupCunck.length == 1 && entry.key == '__default__' - ? const SizedBox.shrink() - : Container( - width: double.infinity, - padding: EdgeInsets.only(left: paddingNum, top: paddingNum, right: paddingNum), - color: TDTheme.of(context).whiteColor1, - child: TDText(entry.key == '__default__' ? context.resource.other : entry.key), + Container( + color: TDTheme.of(context).whiteColor1, + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: widget.minContentHeight ?? 0.0, maxHeight: maxContentHeight), + child: SingleChildScrollView( + child: Column( + children: List.generate(groupCunck.length, (index) { + var entry = groupCunck.entries.elementAt(index); + var chunks = entry.value; + return Column( + children: [ + groupCunck.length == 1 && entry.key == '__default__' + ? const SizedBox.shrink() + : Container( + width: double.infinity, + padding: EdgeInsets.only(left: paddingNum, top: paddingNum, right: paddingNum), + color: TDTheme.of(context).whiteColor1, + child: TDText(entry.key == '__default__' ? context.resource.other : entry.key), + ), + Container( + padding: EdgeInsets.all(paddingNum), + color: TDTheme.of(context).whiteColor1, + child: TDCheckboxGroupContainer( + selectIds: _getSelected(widget.options).map((e) => e!.value).toList(), + onCheckBoxGroupChange: (values) { + _handleSelectChange(values, options: chunks.expand((chunk) => chunk).toList()); + }, + child: Column( + children: List.generate(chunks.length, (ri) { + var num = _num(chunks[ri], widget.optionsColumns); + return Padding( + padding: _getPadding(chunks.length, ri, 'bottom'), + child: Row( + children: List.generate(num, (ci) { + return Expanded( + child: Padding( + padding: _getPadding(num, ci, 'right'), + child: _getCheckboxItem(chunks[ri], ci), + ), + ); + }), + ), + ); + }), ), - Container( - padding: EdgeInsets.all(paddingNum), - color: TDTheme.of(context).whiteColor1, - child: TDCheckboxGroupContainer( - selectIds: _getSelected(widget.options).map((e) => e!.value).toList(), - onCheckBoxGroupChange: (values) { - _handleSelectChange(values, options: chunks.expand((chunk) => chunk).toList()); - }, - child: Column( - children: List.generate(chunks.length, (ri) { - var num = _num(chunks[ri], widget.optionsColumns); - return Padding( - padding: _getPadding(chunks.length, ri, 'bottom'), - child: Row( - children: List.generate(num, (ci) { - return Expanded( - child: Padding( - padding: _getPadding(num, ci, 'right'), - child: _getCheckboxItem(chunks[ri], ci), - ), - ); - }), - ), - ); - }), ), ), - ), - ], - ); - }), + ], + ); + }), + ), ), ), ), @@ -198,10 +213,13 @@ class _TDDropdownItemState extends State { ), ); return widget.minContentHeight != null || widget.maxContentHeight != null - ? ConstrainedBox( - constraints: BoxConstraints( - minHeight: widget.minContentHeight ?? 0.0, maxHeight: widget.maxContentHeight ?? double.infinity), - child: widget.maxContentHeight != null ? SingleChildScrollView(child: radios) : radios, + ? Container( + color: TDTheme.of(context).whiteColor1, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: widget.minContentHeight ?? 0.0, maxHeight: widget.maxContentHeight ?? double.infinity), + child: widget.maxContentHeight != null ? SingleChildScrollView(child: radios) : radios, + ), ) : radios; } @@ -267,15 +285,12 @@ class _TDDropdownItemState extends State { child: TDButton( text: context.resource.reset, theme: TDButtonTheme.light, - // disabled: selectIds.isEmpty, onTap: () { widget.options?.forEach((element) { element.selected = false; }); setState(() {}); - if (widget.onReset != null) { - widget.onReset!(); - } + widget.onReset?.call(); }, ), ), @@ -284,12 +299,9 @@ class _TDDropdownItemState extends State { child: TDButton( text: context.resource.confirm, theme: TDButtonTheme.primary, - // disabled: selectIds.isEmpty, onTap: () { _handleClose(); - if (widget.onConfirm != null) { - widget.onConfirm!(_getSelected(widget.options).map((e) => e!.value).toList()); - } + widget.onConfirm?.call(_getSelected(widget.options).map((e) => e!.value).toList()); }, ), ), @@ -325,9 +337,7 @@ class _TDDropdownItemState extends State { (options ?? widget.options)?.forEach((element) { element.selected = selected is List ? selected.contains(element.value) : element.value == selected; }); - if (widget.onChange != null) { - widget.onChange!(_getSelected(widget.options).map((e) => e!.value).toList()); - } + widget.onChange?.call(_getSelected(widget.options).map((e) => e!.value).toList()); if (widget.multiple != true) { _handleClose(); } @@ -337,10 +347,7 @@ class _TDDropdownItemState extends State { if (widget.multiple != true) { await Future.delayed(const Duration(milliseconds: 100)); } - var handleClose = popupState.handleClose; - if (handleClose != null) { - unawaited(handleClose()); - } + await Navigator.maybePop(context); } } diff --git a/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_menu.dart b/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_menu.dart index 76dda333e..de0045961 100644 --- a/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_menu.dart +++ b/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_menu.dart @@ -25,6 +25,9 @@ enum TDDropdownMenuDirection { /// 下拉菜单构建器 typedef TDDropdownItemBuilder = List Function(BuildContext context); +/// 自定义标签内容 +typedef LabelBuilder = Widget Function(BuildContext context, String label, bool isOpened, int index); + /// 下拉菜单 class TDDropdownMenu extends StatefulWidget { const TDDropdownMenu({ @@ -35,10 +38,14 @@ class TDDropdownMenu extends StatefulWidget { this.direction = TDDropdownMenuDirection.auto, this.duration = 200.0, this.showOverlay = true, - this.isScrollable=false, + this.isScrollable = false, this.arrowIcon, + this.labelBuilder, this.onMenuOpened, this.onMenuClosed, + this.width, + this.height = 48, + this.tabBarAlign = MainAxisAlignment.center, }) : super(key: key); /// 下拉菜单构建器,优先级高于[items] @@ -62,16 +69,27 @@ class TDDropdownMenu extends StatefulWidget { /// 自定义箭头图标 final IconData? arrowIcon; + /// 自定义标签内容 + final LabelBuilder? labelBuilder; + /// 展开菜单事件 final ValueChanged? onMenuOpened; /// 关闭菜单事件 final ValueChanged? onMenuClosed; - static _TDDropdownMenuState? _currentOpenedInstance; - /// 是否开启滚动列表 final bool isScrollable; + + /// menu的宽度 + final double? width; + + /// menu的高度 + final double? height; + + /// [TDDropdownItem.label]和[arrowIcon]/[TDDropdownItem.arrowIcon]的对齐方式 + final MainAxisAlignment? tabBarAlign; + @override _TDDropdownMenuState createState() => _TDDropdownMenuState(); } @@ -91,16 +109,13 @@ class _TDDropdownMenuState extends State with TickerProviderStat @override void dispose() { - _dropdownPopup?.overlayEntry?.remove(); - _dropdownPopup?.overlayEntry = null; - TDDropdownMenu._currentOpenedInstance = null; _iconControllers?.forEach((controller) { controller.dispose(); }); super.dispose(); } - @override + @override void didUpdateWidget(TDDropdownMenu oldWidget) { super.didUpdateWidget(oldWidget); if (widget.builder != oldWidget.builder || widget.items != oldWidget.items) { @@ -110,72 +125,44 @@ class _TDDropdownMenuState extends State with TickerProviderStat @override Widget build(BuildContext context) { - Widget tabBar=Row( + Widget tabBar = Row( children: List.generate( _items?.length ?? 0, - (index) { + (index) { return Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (_disabled(index)) { - return; - } - _isOpened[index] ? _closeMenu() : _openMenu(index); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [_getText(index), _getIcon(index)], - ), - ), + child: _tabBarContent(index), ); }, ), ); - if(widget.isScrollable){ - tabBar=SingleChildScrollView( + if (widget.isScrollable) { + tabBar = SingleChildScrollView( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), - child:Row( + child: Row( children: List.generate( _items?.length ?? 0, - (index) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (_disabled(index)) { - return; - } - _isOpened[index] ? _closeMenu() : _openMenu(index); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [_getText(index), _getIcon(index)], - ), - ); - }, + (index) => SizedBox( + width: _items![index].tabBarWidth, + child: _tabBarContent(index), + ), ), ), ); } - return WillPopScope( - onWillPop: () async { - var isClose = await _closeMenu(); - return !isClose; - }, - child: Container( - height: 48, - decoration: BoxDecoration( - color: TDTheme.of(context).whiteColor1, - border: Border( - bottom: BorderSide( - color: TDTheme.of(context).grayColor3, - width: 1, - ), + return Container( + height: widget.height, + width: widget.width ?? double.infinity, + decoration: BoxDecoration( + color: TDTheme.of(context).whiteColor1, + border: Border( + bottom: BorderSide( + color: TDTheme.of(context).grayColor3, + width: 1, ), ), - child: tabBar, ), + child: tabBar, ); } @@ -199,17 +186,44 @@ class _TDDropdownMenuState extends State with TickerProviderStat _iconAnimations = _iconControllers?.map((e) => Tween(begin: 0, end: 0.5).animate(e)).toList() ?? []; } + Widget _tabBarContent(int index) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + if (_disabled(index)) { + return; + } + _isOpened[index] ? await Navigator.maybePop(context) : _openMenu(index); + }, + child: Row( + mainAxisAlignment: _items![index].tabBarAlign ?? widget.tabBarAlign ?? MainAxisAlignment.center, + children: [_getText(index), _getIcon(index)], + ), + ); + } + Widget _getText(int index) { + final label = _items![index].getLabel(); + if (widget.labelBuilder != null) { + return widget.labelBuilder!(context, label, _isOpened[index], index); + } var textColor = _disabled(index) ? TDTheme.of(context).fontGyColor4 : _isOpened[index] ? TDTheme.of(context).brandColor7 : TDTheme.of(context).fontGyColor1; - return TDText(_items![index].getLabel(), font: TDTheme.of(context).fontBodyMedium, textColor: textColor); + return TDText( + label, + font: TDTheme.of(context).fontBodyMedium, + textColor: textColor, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); } Widget _getIcon(int index) { - var arrowIcon = widget.arrowIcon ?? + var arrowIcon = _items![index].arrowIcon ?? + widget.arrowIcon ?? (widget.direction == TDDropdownMenuDirection.up ? TDIcons.caret_up_small : TDIcons.caret_down_small); return RotationTransition( turns: _iconAnimations[index], @@ -231,8 +245,10 @@ class _TDDropdownMenuState extends State with TickerProviderStat /// 打开菜单 void _openMenu(int index) async { - await TDDropdownMenu._currentOpenedInstance?._closeMenu(); - TDDropdownMenu._currentOpenedInstance = this; + /// 如果已经打开了,则关闭 + if (_isOpened.contains(true)) { + await Navigator.maybePop(context); + } _dropdownPopup ??= TDDropdownPopup( child: _items![index], parentContext: context, @@ -243,9 +259,7 @@ class _TDDropdownMenuState extends State with TickerProviderStat duration: Duration(milliseconds: (widget.duration ?? 200).toInt()), ); unawaited(_dropdownPopup!.add(_items![index]).then((value) { - if (widget.onMenuOpened != null) { - widget.onMenuOpened!(index); - } + widget.onMenuOpened?.call(index); })); _isOpened = List.filled(_items?.length ?? 0, false); @@ -261,10 +275,10 @@ class _TDDropdownMenuState extends State with TickerProviderStat } /// 关闭菜单 - Future _closeMenu() async { + Future _closeMenu() async { var index = _isOpened.indexOf(true); if (index < 0) { - return false; + return; } _isOpened = List.filled(_items?.length ?? 0, false); setState(() {}); @@ -274,10 +288,8 @@ class _TDDropdownMenuState extends State with TickerProviderStat } }); await _dropdownPopup?.remove(); - TDDropdownMenu._currentOpenedInstance = null; if (index >= 0 && widget.onMenuClosed != null) { widget.onMenuClosed!(index); } - return true; } } diff --git a/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_panel.dart b/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_panel.dart index 398de14e8..9636bdfc1 100644 --- a/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_panel.dart +++ b/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_panel.dart @@ -6,8 +6,6 @@ import 'package:flutter/material.dart'; import 'td_dropdown_menu.dart'; import 'td_dropdown_popup.dart'; -typedef FutureParamCallback = void Function(Future Function()); - class TDDropdownPanel extends StatefulWidget { const TDDropdownPanel({ Key? key, @@ -18,7 +16,7 @@ class TDDropdownPanel extends StatefulWidget { required this.directionListenable, required this.colorAlphaListenable, required this.direction, - required this.closeCallback, + required this.closeListenable, required this.onOpened, required this.child, }) : super(key: key); @@ -30,7 +28,7 @@ class TDDropdownPanel extends StatefulWidget { final ValueNotifier directionListenable; final ValueNotifier colorAlphaListenable; final TDDropdownPopupDirection direction; - final FutureParamCallback closeCallback; + final ValueNotifier closeListenable; final VoidCallback onOpened; final Widget child; @@ -46,7 +44,15 @@ class _TDDropdownPanelState extends State with SingleTickerProv void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: widget.duration); - widget.closeCallback(close); + widget.closeListenable.value = close; + } + + @override + void didUpdateWidget(TDDropdownPanel oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.directionListenable != oldWidget.directionListenable) { + widget.closeListenable.value = close; + } } @override @@ -112,6 +118,7 @@ class _TDDropdownPanelState extends State with SingleTickerProv WidgetsBinding.instance.addPostFrameCallback((_) { if (_controller.status == AnimationStatus.dismissed) { widget.colorAlphaListenable.value = true; + _controller.duration = widget.duration; _controller.forward().whenCompleteOrCancel(() { widget.onOpened(); }); @@ -129,6 +136,7 @@ class _TDDropdownPanelState extends State with SingleTickerProv Future close() { widget.colorAlphaListenable.value = false; + _controller.duration = widget.duration ~/ 2; return _controller.reverse(); } } diff --git a/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_popup.dart b/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_popup.dart index 467c3bf30..536901daf 100644 --- a/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_popup.dart +++ b/tdesign-component/lib/src/components/dropdown_menu/td_dropdown_popup.dart @@ -14,7 +14,7 @@ class TDDropdownPopup { TDDropdownPopup({ required this.parentContext, required this.child, - this.handleClose, + required this.handleClose, this.direction = TDDropdownPopupDirection.auto, this.showOverlay = true, this.closeOnClickOverlay = true, @@ -23,31 +23,33 @@ class TDDropdownPopup { final BuildContext parentContext; final TDDropdownItem child; - final FutureCallback? handleClose; + final FutureCallback handleClose; final TDDropdownPopupDirection? direction; final bool? showOverlay; final bool? closeOnClickOverlay; final Duration? duration; /// _overlay1:下拉方向的 - /// _overlay2:menu部分的 - /// _overlay3:下拉反方向的 - /// _overlay3Height:下拉反方向的高度,用于判断auto方向 - /// _initContent:初始内容 late double _overlay1Top, _overlay1Bottom, + + /// _overlay2:menu部分的 _overlay2Top, _overlay2Bottom, + + /// _overlay3:下拉反方向的 _overlay3Top, _overlay3Bottom, + + /// _overlay3Height:下拉反方向的高度,用于判断auto方向 _overlay3Height, + + /// _initContent:初始内容 _initContentTop, _initContentBottom; - late Future Function() _closeContent; + final _closeListenable = ValueNotifier(null); final _directionListenable = ValueNotifier(TDDropdownPopupDirection.auto); - final _colorAlphaListenable = ValueNotifier(false); - - OverlayEntry? overlayEntry; + final _colorAlphaListenable = ValueNotifier(false); Duration get _duration => duration ?? const Duration(milliseconds: 200); @@ -88,8 +90,7 @@ class TDDropdownPopup { Future add([TDDropdownItem? updateChild]) { var completer = Completer(); _directionListenable.value = direction ?? TDDropdownPopupDirection.auto; - overlayEntry?.remove(); - overlayEntry = OverlayEntry( + final overlayEntry = OverlayEntry( builder: (BuildContext context) { return _directionListenable.value == TDDropdownPopupDirection.auto ? ValueListenableBuilder( @@ -102,8 +103,7 @@ class TDDropdownPopup { : _getPopup(_directionListenable.value, updateChild, completer); }, ); - - Overlay.of(parentContext).insert(overlayEntry!); + Navigator.push(parentContext, _PopupOverlayRoute(overlayEntry, handleClose)); return completer.future; } @@ -120,22 +120,23 @@ class TDDropdownPopup { _getOverlay3(barrier), ], TDDropdownInherited( - popupState: this, + popupState: this, + directionListenable: _directionListenable, + child: TDDropdownPanel( + duration: _duration, + direction: value, directionListenable: _directionListenable, - child: TDDropdownPanel( - duration: _duration, - direction: value, - directionListenable: _directionListenable, - colorAlphaListenable: _colorAlphaListenable, - initContentBottom: _initContentBottom, - initContentTop: _initContentTop, - reverseHeight: _overlay3Height, - closeCallback: _closeCallback, - onOpened: () { - completer.complete(); - }, - child: updateChild ?? child, - )), + colorAlphaListenable: _colorAlphaListenable, + initContentBottom: _initContentBottom, + initContentTop: _initContentTop, + reverseHeight: _overlay3Height, + closeListenable: _closeListenable, + onOpened: () { + completer.complete(); + }, + child: updateChild ?? child, + ), + ), ]); } @@ -150,7 +151,7 @@ class TDDropdownPopup { builder: (BuildContext context, value, Widget? child) { return AnimatedContainer( color: value ? Colors.black54 : Colors.black54.withAlpha(0), - duration: _duration, + duration: value ? _duration : _duration ~/ 2, child: barrier, ); }, @@ -188,20 +189,29 @@ class TDDropdownPopup { if (!(closeOnClickOverlay ?? true)) { return; } - if (handleClose != null) { - handleClose!(); - } else { - remove(); - } + Navigator.maybePop(parentContext); } Future remove() async { - await _closeContent(); - overlayEntry?.remove(); - overlayEntry = null; + await _closeListenable.value?.call(); + _closeListenable.value = null; + } +} + +class _PopupOverlayRoute extends OverlayRoute { + final OverlayEntry overlayEntry; + final FutureCallback handleClose; + + _PopupOverlayRoute(this.overlayEntry, this.handleClose); + + @override + Iterable createOverlayEntries() { + return [overlayEntry]; } - void _closeCallback(Future Function() fn) { - _closeContent = fn; + @override + Future willPop() async { + await handleClose(); + return super.willPop(); } } diff --git a/tdesign-component/lib/src/components/image_viewer/td_image_viewer_widget.dart b/tdesign-component/lib/src/components/image_viewer/td_image_viewer_widget.dart index 30f8aafd5..ebd31f6e7 100644 --- a/tdesign-component/lib/src/components/image_viewer/td_image_viewer_widget.dart +++ b/tdesign-component/lib/src/components/image_viewer/td_image_viewer_widget.dart @@ -5,10 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_swiper_null_safety/flutter_swiper_null_safety.dart'; import '../../../tdesign_flutter.dart'; -import '../../theme/td_colors.dart'; -import '../../theme/td_theme.dart'; -import '../icon/td_icons.dart'; -import '../image/td_image.dart'; import '../navbar/td_nav_bar.dart'; typedef OnIndexChange = Function(int index); @@ -77,7 +73,13 @@ class _TDImageViewerWidgetState extends State { @override void initState() { super.initState(); - _index = widget.defaultIndex ?? 1; + if(widget.images.isEmpty) { + throw FlutterError('images must not be empty'); + } + if((widget.defaultIndex ?? 0) > widget.images.length - 1) { + throw FlutterError('defaultIndex must be less than images.length'); + } + _index = (widget.defaultIndex ?? 0) + 1; } Widget _getImage(dynamic image) { @@ -139,8 +141,7 @@ class _TDImageViewerWidgetState extends State { @override Widget build(BuildContext context) { var media = MediaQuery.of(context); - var safeAreaHeight = media.padding.top ?? 0; - var width = media.size.width ?? 0; + var safeAreaHeight = media.padding.top; return Stack( children: [ Positioned( @@ -158,6 +159,7 @@ class _TDImageViewerWidgetState extends State { left: 0, right: 0, child: Swiper( + index: _index - 1, itemBuilder: (BuildContext context, int index) { var image = widget.images[index]; return GestureDetector( @@ -209,9 +211,16 @@ class _TDImageViewerWidgetState extends State { if (widget.deleteBtn ?? false) GestureDetector( onTap: () { + if(widget.images.length == 1) { + throw FlutterError('images must not be empty'); + } widget.images.removeAt(_index - 1); widget.onDelete?.call(_index - 1); - setState(() {}); + setState(() { + if(_index > 1) { + _index--; + } + }); }, child: Icon( TDIcons.delete, diff --git a/tdesign-component/lib/src/components/indexes/sticky_header/sticky_header_render.dart b/tdesign-component/lib/src/components/indexes/sticky_header/sticky_header_render.dart new file mode 100644 index 000000000..e84cd57f7 --- /dev/null +++ b/tdesign-component/lib/src/components/indexes/sticky_header/sticky_header_render.dart @@ -0,0 +1,396 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import 'sticky_header_widget.dart'; +import 'value_layout_builder.dart'; + +/// A sliver with a [RenderBox] as header and a [RenderSliver] as child. +/// +/// The [header] stays pinned when it hits the start of the viewport until +/// the [child] scrolls off the viewport. +class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { + RenderSliverStickyHeader({ + RenderObject? header, + RenderSliver? child, + bool overlapsContent = false, + bool sticky = true, + double pinnedOffset = 0.0, + StickyHeaderController? controller, + }) : _overlapsContent = overlapsContent, + _sticky = sticky, + _pinnedOffset = pinnedOffset, + _controller = controller { + this.header = header as RenderBox?; + this.child = child; + } + + SliverStickyHeaderState? _oldState; + double? _headerExtent; + late bool _isPinned; + + bool get overlapsContent => _overlapsContent; + bool _overlapsContent; + + set overlapsContent(bool value) { + if (_overlapsContent == value) { + return; + } + _overlapsContent = value; + markNeedsLayout(); + } + + bool get sticky => _sticky; + bool _sticky; + + set sticky(bool value) { + if (_sticky == value) { + return; + } + _sticky = value; + markNeedsLayout(); + } + + double get pinnedOffset => _pinnedOffset; + double _pinnedOffset; + + set pinnedOffset(double value) { + if (_pinnedOffset == value) { + return; + } + _pinnedOffset = value; + markNeedsLayout(); + } + + StickyHeaderController? get controller => _controller; + StickyHeaderController? _controller; + + set controller(StickyHeaderController? value) { + if (_controller == value) { + return; + } + if (_controller != null && value != null) { + // We copy the state of the old controller. + value.stickyHeaderScrollOffset = _controller!.stickyHeaderScrollOffset; + } + _controller = value; + } + + /// The render object's header + RenderBox? get header => _header; + RenderBox? _header; + + set header(RenderBox? value) { + if (_header != null) { + dropChild(_header!); + } + _header = value; + if (_header != null) { + adoptChild(_header!); + } + } + + /// The render object's unique child + RenderSliver? get child => _child; + RenderSliver? _child; + + set child(RenderSliver? value) { + if (_child != null) { + dropChild(_child!); + } + _child = value; + if (_child != null) { + adoptChild(_child!); + } + } + + @override + void setupParentData(RenderObject child) { + if (child.parentData is! SliverPhysicalParentData) { + child.parentData = SliverPhysicalParentData(); + } + } + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + if (_header != null) { + _header!.attach(owner); + } + if (_child != null) { + _child!.attach(owner); + } + } + + @override + void detach() { + super.detach(); + if (_header != null) { + _header!.detach(); + } + if (_child != null) { + _child!.detach(); + } + } + + @override + void redepthChildren() { + if (_header != null) { + redepthChild(_header!); + } + if (_child != null) { + redepthChild(_child!); + } + } + + @override + void visitChildren(RenderObjectVisitor visitor) { + if (_header != null) { + visitor(_header!); + } + if (_child != null) { + visitor(_child!); + } + } + + @override + List debugDescribeChildren() { + final result = []; + if (header != null) { + result.add(header!.toDiagnosticsNode(name: 'header')); + } + if (child != null) { + result.add(child!.toDiagnosticsNode(name: 'child')); + } + return result; + } + + double computeHeaderExtent() { + if (header == null) { + return 0.0; + } + assert(header!.hasSize); + switch (constraints.axis) { + case Axis.vertical: + return header!.size.height; + case Axis.horizontal: + return header!.size.width; + } + } + + double? get headerLogicalExtent => overlapsContent ? 0.0 : _headerExtent; + + @override + void performLayout() { + if (header == null && child == null) { + geometry = SliverGeometry.zero; + return; + } + + // One of them is not null. + final axisDirection = applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection); + + if (header != null) { + header!.layout( + BoxValueConstraints( + value: _oldState ?? const SliverStickyHeaderState(0.0, false), + constraints: constraints.asBoxConstraints(), + ), + parentUsesSize: true, + ); + _headerExtent = computeHeaderExtent(); + } + + // Compute the header extent only one time. + final headerExtent = headerLogicalExtent!; + final headerPaintExtent = calculatePaintOffset(constraints, from: 0.0, to: headerExtent); + final headerCacheExtent = calculateCacheOffset(constraints, from: 0.0, to: headerExtent); + + if (child == null) { + geometry = SliverGeometry( + scrollExtent: headerExtent, + maxPaintExtent: headerExtent, + paintExtent: headerPaintExtent, + cacheExtent: headerCacheExtent, + hitTestExtent: headerPaintExtent, + hasVisualOverflow: headerExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0); + } else { + child!.layout( + constraints.copyWith( + scrollOffset: math.max(0.0, constraints.scrollOffset - headerExtent), + cacheOrigin: math.min(0.0, constraints.cacheOrigin + headerExtent), + overlap: math.min(headerExtent, constraints.scrollOffset) + (sticky ? constraints.overlap : 0), + remainingPaintExtent: constraints.remainingPaintExtent - headerPaintExtent, + remainingCacheExtent: constraints.remainingCacheExtent - headerCacheExtent, + ), + parentUsesSize: true, + ); + final childLayoutGeometry = child!.geometry!; + if (childLayoutGeometry.scrollOffsetCorrection != null) { + geometry = SliverGeometry( + scrollOffsetCorrection: childLayoutGeometry.scrollOffsetCorrection, + ); + return; + } + + final double paintExtent = math.min( + headerPaintExtent + math.max(childLayoutGeometry.paintExtent, childLayoutGeometry.layoutExtent), + constraints.remainingPaintExtent, + ); + + geometry = SliverGeometry( + scrollExtent: headerExtent + childLayoutGeometry.scrollExtent, + maxScrollObstructionExtent: sticky ? headerPaintExtent : 0, + paintExtent: paintExtent, + layoutExtent: math.min(headerPaintExtent + childLayoutGeometry.layoutExtent, paintExtent), + cacheExtent: math.min(headerCacheExtent + childLayoutGeometry.cacheExtent, constraints.remainingCacheExtent), + maxPaintExtent: headerExtent + childLayoutGeometry.maxPaintExtent, + hitTestExtent: math.max( + headerPaintExtent + childLayoutGeometry.paintExtent, headerPaintExtent + childLayoutGeometry.hitTestExtent), + hasVisualOverflow: childLayoutGeometry.hasVisualOverflow, + ); + + final childParentData = child!.parentData as SliverPhysicalParentData?; + switch (axisDirection) { + case AxisDirection.up: + childParentData!.paintOffset = Offset.zero; + break; + case AxisDirection.right: + childParentData!.paintOffset = Offset(calculatePaintOffset(constraints, from: 0.0, to: headerExtent), 0.0); + break; + case AxisDirection.down: + childParentData!.paintOffset = Offset(0.0, calculatePaintOffset(constraints, from: 0.0, to: headerExtent)); + break; + case AxisDirection.left: + childParentData!.paintOffset = Offset.zero; + break; + } + } + + if (header != null) { + final headerParentData = header!.parentData as SliverPhysicalParentData?; + final childScrollExtent = child?.geometry?.scrollExtent ?? 0.0; + final headerPosition = sticky + ? math.min(constraints.overlap, + childScrollExtent - constraints.scrollOffset - (overlapsContent ? _headerExtent! : 0.0)) + : -constraints.scrollOffset; + + _isPinned = sticky && + ((constraints.scrollOffset + constraints.overlap) > 0.0 || + constraints.remainingPaintExtent == constraints.viewportMainAxisExtent); + + final headerScrollRatio = ((headerPosition - constraints.overlap).abs() / _headerExtent!); + if (_isPinned && headerScrollRatio <= 1) { + controller?.stickyHeaderScrollOffset = constraints.precedingScrollExtent; + } + // second layout if scroll percentage changed and header is a + // RenderStickyHeaderLayoutBuilder. + if (header is RenderConstrainedLayoutBuilder, RenderBox>) { + final headerScrollRatioClamped = headerScrollRatio.clamp(0.0, 1.0); + + final state = SliverStickyHeaderState(headerScrollRatioClamped, _isPinned); + if (_oldState != state) { + _oldState = state; + header!.layout( + BoxValueConstraints( + value: _oldState!, + constraints: constraints.asBoxConstraints(), + ), + parentUsesSize: true, + ); + } + } + + switch (axisDirection) { + case AxisDirection.up: + headerParentData!.paintOffset = Offset(0.0, geometry!.paintExtent - headerPosition - _headerExtent! - pinnedOffset); + break; + case AxisDirection.down: + headerParentData!.paintOffset = Offset(0.0, headerPosition + pinnedOffset); + break; + case AxisDirection.left: + headerParentData!.paintOffset = Offset(geometry!.paintExtent - headerPosition - _headerExtent! - pinnedOffset, 0.0); + break; + case AxisDirection.right: + headerParentData!.paintOffset = Offset(headerPosition + pinnedOffset, 0.0); + break; + } + } + } + + @override + bool hitTestChildren(SliverHitTestResult result, + {required double mainAxisPosition, required double crossAxisPosition}) { + assert(geometry!.hitTestExtent > 0.0); + final childScrollExtent = child?.geometry?.scrollExtent ?? 0.0; + final headerPosition = sticky + ? math.min(constraints.overlap, + childScrollExtent - constraints.scrollOffset - (overlapsContent ? _headerExtent! : 0.0)) + : -constraints.scrollOffset; + + if (header != null && (mainAxisPosition - headerPosition) <= _headerExtent!) { + final didHitHeader = hitTestBoxChild( + BoxHitTestResult.wrap(SliverHitTestResult.wrap(result)), + header!, + mainAxisPosition: mainAxisPosition - childMainAxisPosition(header) - headerPosition, + crossAxisPosition: crossAxisPosition, + ); + + return didHitHeader || + (_overlapsContent && + child != null && + child!.geometry!.hitTestExtent > 0.0 && + child!.hitTest(result, + mainAxisPosition: mainAxisPosition - childMainAxisPosition(child), + crossAxisPosition: crossAxisPosition)); + } else if (child != null && child!.geometry!.hitTestExtent > 0.0) { + return child!.hitTest(result, + mainAxisPosition: mainAxisPosition - childMainAxisPosition(child), crossAxisPosition: crossAxisPosition); + } + return false; + } + + @override + double childMainAxisPosition(RenderObject? child) { + if (child == header) { + return _isPinned ? 0.0 : -(constraints.scrollOffset + constraints.overlap); + } + if (child == this.child) { + return calculatePaintOffset(constraints, from: 0.0, to: headerLogicalExtent!); + } + return 0; + } + + @override + double? childScrollOffset(RenderObject child) { + assert(child.parent == this); + if (child == this.child) { + return headerLogicalExtent; + } else { + return super.childScrollOffset(child); + } + } + + @override + void applyPaintTransform(RenderObject child, Matrix4 transform) { + final childParentData = child.parentData as SliverPhysicalParentData; + childParentData.applyPaintTransform(transform); + } + + @override + void paint(PaintingContext context, Offset offset) { + if (geometry!.visible) { + if (child != null && child!.geometry!.visible) { + final childParentData = child!.parentData as SliverPhysicalParentData; + context.paintChild(child!, offset + childParentData.paintOffset); + } + + // The header must be drawn over the sliver. + if (header != null) { + final headerParentData = header!.parentData as SliverPhysicalParentData; + context.paintChild(header!, offset + headerParentData.paintOffset); + } + } + } +} diff --git a/tdesign-component/lib/src/components/indexes/sticky_header/sticky_header_widget.dart b/tdesign-component/lib/src/components/indexes/sticky_header/sticky_header_widget.dart new file mode 100644 index 000000000..b1006b243 --- /dev/null +++ b/tdesign-component/lib/src/components/indexes/sticky_header/sticky_header_widget.dart @@ -0,0 +1,314 @@ +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'sticky_header_render.dart'; +import 'value_layout_builder.dart'; + +/// Signature used by [SliverStickyHeader.builder] to build the header +/// when the sticky header state has changed. +typedef SliverStickyHeaderWidgetBuilder = Widget Function( + BuildContext context, + SliverStickyHeaderState state, +); + +// ignore: prefer_mixin +class StickyHeaderController with ChangeNotifier { + /// The offset to use in order to jump to the first item + /// of current the sticky header. + /// + /// If there is no sticky headers, this is 0. + double get stickyHeaderScrollOffset => _stickyHeaderScrollOffset; + double _stickyHeaderScrollOffset = 0; + + /// This setter should only be used by flutter_sticky_header package. + set stickyHeaderScrollOffset(double value) { + if (_stickyHeaderScrollOffset != value) { + _stickyHeaderScrollOffset = value; + notifyListeners(); + } + } +} + +/// The [StickyHeaderController] for descendant widgets that don't specify one +/// explicitly. +/// +/// [DefaultStickyHeaderController] is an inherited widget that is used to share a +/// [StickyHeaderController] with [SliverStickyHeader]s. It's used when sharing an +/// explicitly created [StickyHeaderController] isn't convenient because the sticky +/// headers are created by a stateless parent widget or by different parent +/// widgets. +class DefaultStickyHeaderController extends StatefulWidget { + const DefaultStickyHeaderController({ + Key? key, + required this.child, + }) : super(key: key); + + /// The widget below this widget in the tree. + /// + /// Typically a [Scaffold] whose [AppBar] includes a [TabBar]. + /// + /// {@macro flutter.widgets.child} + final Widget child; + + /// The closest instance of this class that encloses the given context. + /// + /// Typical usage: + /// + /// ```dart + /// StickyHeaderController controller = DefaultStickyHeaderController.of(context); + /// ``` + static StickyHeaderController? of(BuildContext context) { + final scope = context.dependOnInheritedWidgetOfExactType<_StickyHeaderControllerScope>(); + return scope?.controller; + } + + @override + _DefaultStickyHeaderControllerState createState() => _DefaultStickyHeaderControllerState(); +} + +class _DefaultStickyHeaderControllerState extends State { + StickyHeaderController? _controller; + + @override + void initState() { + super.initState(); + _controller = StickyHeaderController(); + } + + @override + void dispose() { + _controller!.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _StickyHeaderControllerScope( + controller: _controller, + child: widget.child, + ); + } +} + +class _StickyHeaderControllerScope extends InheritedWidget { + const _StickyHeaderControllerScope({ + Key? key, + this.controller, + required Widget child, + }) : super(key: key, child: child); + + final StickyHeaderController? controller; + + @override + bool updateShouldNotify(_StickyHeaderControllerScope old) { + return controller != old.controller; + } +} + +/// State describing how a sticky header is rendered. +@immutable +class SliverStickyHeaderState { + const SliverStickyHeaderState( + this.scrollPercentage, + this.isPinned, + ); + + final double scrollPercentage; + + final bool isPinned; + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) { + return true; + } + if (other is! SliverStickyHeaderState) { + return false; + } + final typedOther = other; + return scrollPercentage == typedOther.scrollPercentage && isPinned == typedOther.isPinned; + } + + @override + int get hashCode { + return Object.hash(scrollPercentage, isPinned); + } +} + +/// A sliver that displays a header before its sliver. +/// The header scrolls off the viewport only when the sliver does. +/// +/// Place this widget inside a [CustomScrollView] or similar. +class SliverStickyHeader extends RenderObjectWidget { + /// Creates a sliver that displays the [header] before its [sliver], unless + /// [overlapsContent] it's true. + /// The [header] stays pinned when it hits the start of the viewport until + /// the [sliver] scrolls off the viewport. + /// + /// The [overlapsContent] and [sticky] arguments must not be null. + /// + /// If a [StickyHeaderController] is not provided, then the value of + /// [DefaultStickyHeaderController.of] will be used. + const SliverStickyHeader({ + Key? key, + this.header, + this.sliver, + this.overlapsContent = false, + this.sticky = true, + this.pinnedOffset = 0.0, + this.controller, + }) : super(key: key); + + /// Creates a widget that builds the header of a [SliverStickyHeader] + /// each time its scroll percentage changes. + /// + /// The [builder], [overlapsContent] and [sticky] arguments must not be null. + /// + /// If a [StickyHeaderController] is not provided, then the value of + /// [DefaultStickyHeaderController.of] will be used. + SliverStickyHeader.builder({ + Key? key, + required SliverStickyHeaderWidgetBuilder builder, + Widget? sliver, + bool overlapsContent = false, + bool sticky = true, + double pinnedOffset = 0.0, + StickyHeaderController? controller, + }) : this( + key: key, + header: ValueLayoutBuilder( + builder: (context, constraints) => builder(context, constraints.value), + ), + sliver: sliver, + overlapsContent: overlapsContent, + sticky: sticky, + pinnedOffset: pinnedOffset, + controller: controller, + ); + + /// The header to display before the sliver. + final Widget? header; + + /// The sliver to display after the header. + final Widget? sliver; + + /// Whether the header should be drawn on top of the sliver + /// instead of before. + final bool overlapsContent; + + /// Whether to stick the header. + /// Defaults to true. + final bool sticky; + + /// The offset at which to pin the header. + /// Defaults to 0.0. + final double pinnedOffset; + + /// The controller used to interact with this sliver. + /// + /// If a [StickyHeaderController] is not provided, then the value of [DefaultStickyHeaderController.of] + /// will be used. + final StickyHeaderController? controller; + + @override + RenderSliverStickyHeader createRenderObject(BuildContext context) { + return RenderSliverStickyHeader( + overlapsContent: overlapsContent, + sticky: sticky, + pinnedOffset: sticky ? pinnedOffset : 0.0, + controller: controller ?? DefaultStickyHeaderController.of(context), + ); + } + + @override + SliverStickyHeaderRenderObjectElement createElement() => SliverStickyHeaderRenderObjectElement(this); + + @override + void updateRenderObject( + BuildContext context, + RenderSliverStickyHeader renderObject, + ) { + renderObject + ..overlapsContent = overlapsContent + ..sticky = sticky + ..pinnedOffset = sticky ? pinnedOffset : 0.0 + ..controller = controller ?? DefaultStickyHeaderController.of(context); + } +} + +class SliverStickyHeaderRenderObjectElement extends RenderObjectElement { + /// Creates an element that uses the given widget as its configuration. + SliverStickyHeaderRenderObjectElement(SliverStickyHeader widget) : super(widget); + + @override + SliverStickyHeader get widget => super.widget as SliverStickyHeader; + + Element? _header; + + Element? _sliver; + + @override + void visitChildren(ElementVisitor visitor) { + if (_header != null) { + visitor(_header!); + } + if (_sliver != null) { + visitor(_sliver!); + } + } + + @override + void forgetChild(Element child) { + super.forgetChild(child); + if (child == _header) { + _header = null; + } + if (child == _sliver) { + _sliver = null; + } + } + + @override + void mount(Element? parent, dynamic newSlot) { + super.mount(parent, newSlot); + _header = updateChild(_header, widget.header, 0); + _sliver = updateChild(_sliver, widget.sliver, 1); + } + + @override + void update(SliverStickyHeader newWidget) { + super.update(newWidget); + assert(widget == newWidget); + _header = updateChild(_header, widget.header, 0); + _sliver = updateChild(_sliver, widget.sliver, 1); + } + + @override + void insertRenderObjectChild(RenderObject child, int? slot) { + final renderObject = this.renderObject as RenderSliverStickyHeader; + if (slot == 0) { + renderObject.header = child as RenderBox?; + } + if (slot == 1) { + renderObject.child = child as RenderSliver?; + } + assert(renderObject == this.renderObject); + } + + @override + void moveRenderObjectChild(RenderObject child, slot, newSlot) { + assert(false); + } + + @override + void removeRenderObjectChild(RenderObject child, slot) { + final renderObject = this.renderObject as RenderSliverStickyHeader; + if (renderObject.header == child) { + renderObject.header = null; + } + if (renderObject.child == child) { + renderObject.child = null; + } + assert(renderObject == this.renderObject); + } +} diff --git a/tdesign-component/lib/src/components/indexes/sticky_header/value_layout_builder.dart b/tdesign-component/lib/src/components/indexes/sticky_header/value_layout_builder.dart new file mode 100644 index 000000000..22f377ae3 --- /dev/null +++ b/tdesign-component/lib/src/components/indexes/sticky_header/value_layout_builder.dart @@ -0,0 +1,130 @@ +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +/// The signature of the [ValueLayoutBuilder] builder function. +typedef ValueLayoutWidgetBuilder = Widget Function( + BuildContext context, + BoxValueConstraints constraints, +); + +class BoxValueConstraints extends BoxConstraints { + BoxValueConstraints({ + required this.value, + required BoxConstraints constraints, + }) : super( + minWidth: constraints.minWidth, + maxWidth: constraints.maxWidth, + minHeight: constraints.minHeight, + maxHeight: constraints.maxHeight, + ); + + final T value; + + @override + bool operator ==(dynamic other) { + assert(debugAssertIsValid()); + if (identical(this, other)) { + return true; + } + if (other is! BoxValueConstraints) { + return false; + } + final typedOther = other; + assert(typedOther.debugAssertIsValid()); + return value == typedOther.value && + minWidth == typedOther.minWidth && + maxWidth == typedOther.maxWidth && + minHeight == typedOther.minHeight && + maxHeight == typedOther.maxHeight; + } + + @override + int get hashCode { + assert(debugAssertIsValid()); + return Object.hash(minWidth, maxWidth, minHeight, maxHeight, value); + } +} + +/// Builds a widget tree that can depend on the parent widget's size and a extra +/// value. +/// +/// Similar to the [LayoutBuilder] widget except that the constraints contains +/// an extra value. +/// +class ValueLayoutBuilder extends ConstrainedLayoutBuilder> { + /// Creates a widget that defers its building until layout. + const ValueLayoutBuilder({ + Key? key, + required ValueLayoutWidgetBuilder builder, + }) : super(key: key, builder: builder); + + @override + ValueLayoutWidgetBuilder get builder => super.builder; + + @override + _RenderValueLayoutBuilder createRenderObject(BuildContext context) => _RenderValueLayoutBuilder(); +} + +class _RenderValueLayoutBuilder extends RenderBox + with RenderObjectWithChildMixin, RenderConstrainedLayoutBuilder, RenderBox> { + @override + double computeMinIntrinsicWidth(double height) { + assert(_debugThrowIfNotCheckingIntrinsics()); + return 0.0; + } + + @override + double computeMaxIntrinsicWidth(double height) { + assert(_debugThrowIfNotCheckingIntrinsics()); + return 0.0; + } + + @override + double computeMinIntrinsicHeight(double width) { + assert(_debugThrowIfNotCheckingIntrinsics()); + return 0.0; + } + + @override + double computeMaxIntrinsicHeight(double width) { + assert(_debugThrowIfNotCheckingIntrinsics()); + return 0.0; + } + + @override + void performLayout() { + final constraints = this.constraints; + rebuildIfNecessary(); + if (child != null) { + child!.layout(constraints, parentUsesSize: true); + size = constraints.constrain(child!.size); + } else { + size = constraints.biggest; + } + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + return child?.hitTest(result, position: position) ?? false; + } + + @override + void paint(PaintingContext context, Offset offset) { + if (child != null) { + context.paintChild(child!, offset); + } + } + + bool _debugThrowIfNotCheckingIntrinsics() { + assert(() { + if (!RenderObject.debugCheckingIntrinsics) { + throw FlutterError('ValueLayoutBuilder does not support returning intrinsic dimensions.\n' + 'Calculating the intrinsic dimensions would require running the layout ' + 'callback speculatively, which might mutate the live render object tree.'); + } + return true; + }()); + + return true; + } +} diff --git a/tdesign-component/lib/src/components/indexes/td_indexes.dart b/tdesign-component/lib/src/components/indexes/td_indexes.dart new file mode 100644 index 000000000..bd5b32e25 --- /dev/null +++ b/tdesign-component/lib/src/components/indexes/td_indexes.dart @@ -0,0 +1,216 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../../../tdesign_flutter.dart'; +import '../../util/iterable_ext.dart'; + +export 'sticky_header/sticky_header_widget.dart'; +export 'td_indexes_anchor.dart'; +export 'td_indexes_list.dart'; + +/// 索引 +class TDIndexes extends StatefulWidget { + const TDIndexes({ + Key? key, + this.indexList, + this.indexListMaxHeight = 0.8, + this.sticky = true, + this.stickyOffset = 0, + this.capsuleTheme = false, + this.reverse = false, + this.scrollController, + this.onChange, + this.onSelect, + required this.builderContent, + this.builderAnchor, + this.builderIndex, + }) : super(key: key); + + /// 索引字符列表。不传默认 A-Z + final List? indexList; + + /// 索引列表最大高度(父容器高度的百分比,默认0.8) + final double? indexListMaxHeight; + + /// 锚点是否吸顶 + final bool? sticky; + + /// 锚点吸顶时与顶部的距离 + final double? stickyOffset; + + /// 锚点是否为胶囊式样式 + final bool? capsuleTheme; + + /// 反方向滚动置顶 + final bool? reverse; + + /// 滚动控制器 + final ScrollController? scrollController; + + /// 索引发生变更时触发事件 + final void Function(String index)? onChange; + + /// 点击侧边栏时触发事件 + final void Function(String index)? onSelect; + + /// 内容自定义构建 + final Widget? Function(BuildContext context, String index) builderContent; + + /// 锚点自定义构建 + final Widget? Function(BuildContext context, String index, bool isPinnedToTop)? builderAnchor; + + /// 索引文本自定义构建,包括索引激活左侧提示 + final Widget Function(BuildContext context, String index, bool isActive)? builderIndex; + + @override + _TDIndexesState createState() => _TDIndexesState(); +} + +class _TDIndexesState extends State { + late List _indexList; + late ValueNotifier _activeIndex; + late ScrollController _scrollController; + final _anchorKeys = {}; + final _contentKeys = {}; + var _isAnimating = false; + + @override + void initState() { + super.initState(); + _indexList = widget.indexList ?? _azList(); + _activeIndex = ValueNotifier(_indexList.getOrNull(0) ?? ''); + _scrollController = widget.scrollController ?? ScrollController(); + } + + @override + void didUpdateWidget(TDIndexes oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.indexList != oldWidget.indexList) { + _indexList = widget.indexList ?? _azList(); + _activeIndex = ValueNotifier(_indexList.getOrNull(0) ?? ''); + } + if (widget.scrollController != oldWidget.scrollController) { + _scrollController.dispose(); + _scrollController = widget.scrollController ?? ScrollController(); + } + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + color: TDTheme.of(context).whiteColor1, + child: Stack( + children: [ + CustomScrollView( + controller: _scrollController, + reverse: widget.reverse ?? false, + slivers: _slivers(), + ), + TDIndexesList( + indexList: _indexList, + activeIndex: _activeIndex, + onSelect: (newIndex, oldIndex) { + widget.onSelect?.call(newIndex); + widget.onChange?.call(newIndex); + _scrollToTarget(newIndex, oldIndex); + }, + indexListMaxHeight: widget.indexListMaxHeight ?? 0.8, + builderIndex: widget.builderIndex, + ), + ], + ), + ); + } + + List _slivers() { + final capsuleTheme = widget.capsuleTheme ?? false; + final stickyOffset = widget.stickyOffset ?? 0; + _anchorKeys.clear(); + _contentKeys.clear(); + return _indexList.map((e) { + final isPinnedOffset = capsuleTheme && _activeIndex.value == e; + return SliverStickyHeader.builder( + sticky: widget.sticky ?? true, + pinnedOffset: isPinnedOffset ? TDTheme.of(context).spacer8 + stickyOffset : stickyOffset, + builder: (context, state) { + _anchorKeys[e] = context; + if (state.isPinned && _activeIndex.value != e && !_isAnimating) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _activeIndex.value = e; + widget.onChange?.call(e); + }); + } + return TDIndexesAnchor( + text: e, + capsuleTheme: capsuleTheme, + activeIndex: _activeIndex, + builderAnchor: widget.builderAnchor, + sticky: widget.sticky ?? true, + ); + }, + sliver: SliverToBoxAdapter( + child: Builder( + builder: (context) { + _contentKeys[e] = context; + return Padding( + padding: isPinnedOffset ? EdgeInsets.only(top: TDTheme.of(context).spacer8) : EdgeInsets.zero, + child: widget.builderContent(context, e), + ); + }, + ), + ), + ); + }).toList(); + } + + List _azList() { + final azList = []; + for (var i = 65; i <= 90; i++) { + azList.add(String.fromCharCode(i)); + } + return azList; + } + + void _scrollToTarget(String newIndex, String oldIndex) { + _isAnimating = true; + + /// isUp: 是否(手指)向上滑动 + final isUp = _indexList.indexOf(newIndex) > _indexList.indexOf(oldIndex); + if (isUp) { + var index = oldIndex; + final contentRenderBox = _contentKeys[index]?.findRenderObject() as RenderBox?; + if (contentRenderBox != null) { + final contentHeight = contentRenderBox.size.height; + final maxScrollExtent = _scrollController.position.maxScrollExtent; + final targetOffset = + contentRenderBox.localToGlobal(Offset(0, contentHeight), ancestor: context.findRenderObject()); + final scrollOffset = targetOffset.dy + _scrollController.offset; + _scrollController.jumpTo(min(maxScrollExtent, scrollOffset)); + } + index = _indexList[_indexList.indexOf(index) + 1]; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (index != newIndex) { + _scrollToTarget(newIndex, index); + } else { + _isAnimating = false; + } + }); + } else { + final anchorContext = _anchorKeys[newIndex]; + if (anchorContext != null) { + Scrollable.ensureVisible(anchorContext).then((value) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _isAnimating = false; + }); + }); + } + } + } +} diff --git a/tdesign-component/lib/src/components/indexes/td_indexes_anchor.dart b/tdesign-component/lib/src/components/indexes/td_indexes_anchor.dart new file mode 100644 index 000000000..8e80163c7 --- /dev/null +++ b/tdesign-component/lib/src/components/indexes/td_indexes_anchor.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import '../../../tdesign_flutter.dart'; + +/// 索引锚点 +class TDIndexesAnchor extends StatelessWidget { + const TDIndexesAnchor({ + Key? key, + required this.sticky, + required this.text, + required this.capsuleTheme, + this.builderAnchor, + required this.activeIndex, + }) : super(key: key); + + /// 索引是否吸顶 + final bool sticky; + + /// 锚点文本 + final String text; + + /// 是否为胶囊式样式 + final bool capsuleTheme; + + /// 选中索引 + final ValueNotifier activeIndex; + + /// 索引锚点构建 + final Widget? Function(BuildContext context, String index, bool isPinnedToTop)? builderAnchor; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: activeIndex, + builder: (context, value, child) { + final isPinned = value == text; + final customAnchor = builderAnchor?.call(context, text, isPinned); + return customAnchor ?? + Container( + padding: + EdgeInsets.symmetric(vertical: TDTheme.of(context).spacer4, horizontal: TDTheme.of(context).spacer16), + margin: capsuleTheme ? EdgeInsets.symmetric(horizontal: TDTheme.of(context).spacer8) : null, + decoration: BoxDecoration( + color: isPinned ? TDTheme.of(context).whiteColor1 : TDTheme.of(context).grayColor1, + borderRadius: capsuleTheme ? BorderRadius.circular(TDTheme.of(context).radiusCircle) : null, + border: isPinned + ? capsuleTheme + ? Border.all(color: TDTheme.of(context).grayColor1) + : Border(bottom: BorderSide(color: TDTheme.of(context).grayColor1)) + : null, + ), + child: TDText( + text, + forceVerticalCenter: true, + font: isPinned ? TDTheme.of(context).fontMarkMedium : TDTheme.of(context).fontTitleSmall, + textColor: isPinned ? TDTheme.of(context).brandColor7 : TDTheme.of(context).fontGyColor1, + ), + ); + }, + ); + } +} diff --git a/tdesign-component/lib/src/components/indexes/td_indexes_list.dart b/tdesign-component/lib/src/components/indexes/td_indexes_list.dart new file mode 100644 index 000000000..244491580 --- /dev/null +++ b/tdesign-component/lib/src/components/indexes/td_indexes_list.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; + +import '../../../tdesign_flutter.dart'; +import '../../util/iterable_ext.dart'; +import 'sticky_header/sticky_header_widget.dart'; +import 'td_indexes_anchor.dart'; + +/// 索引 +class TDIndexesList extends StatefulWidget { + const TDIndexesList({ + Key? key, + required this.indexList, + this.indexListMaxHeight = 0.8, + required this.activeIndex, + required this.onSelect, + this.builderIndex, + }) : super(key: key); + + /// 索引字符列表。不传默认 A-Z + final List indexList; + + /// 索引列表最大高度(父容器高度的百分比,默认0.8) + final double indexListMaxHeight; + + /// 选中索引 + final ValueNotifier activeIndex; + + /// 点击侧边栏时触发事件 + final void Function(String newIndex, String oldIndex) onSelect; + + /// 索引文本自定义构建,包括索引激活左侧提示 + final Widget Function(BuildContext context, String index, bool isActive)? builderIndex; + + @override + State createState() => _TDIndexesListState(); +} + +class _TDIndexesListState extends State { + late Map _containerKeys; + final _indexSize = 20.0; + var _showTip = false; + + @override + void initState() { + super.initState(); + _containerKeys = widget.indexList.asMap().map((index, e) => MapEntry(e, GlobalKey())); + } + + @override + void didUpdateWidget(TDIndexesList oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.indexList != oldWidget.indexList) { + _containerKeys = widget.indexList.asMap().map((index, e) => MapEntry(e, GlobalKey())); + } + } + + @override + Widget build(BuildContext context) { + return Positioned( + right: TDTheme.of(context).spacer8, + top: 0, + bottom: 0, + child: Align( + child: FractionallySizedBox( + heightFactor: widget.indexListMaxHeight, + child: Align( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onVerticalDragUpdate: (details) { + _changeSelect(details.globalPosition); + }, + onTapUp: (details) { + _changeSelect(details.globalPosition); + _hideTip(); + }, + onVerticalDragEnd: (details) { + _hideTip(); + }, + child: ValueListenableBuilder( + valueListenable: widget.activeIndex, + builder: (context, value, child) { + return Column( + mainAxisSize: MainAxisSize.min, + children: widget.indexList.map( + (e) { + final isActive = value == e; + if (widget.builderIndex != null) { + return widget.builderIndex!(context, e, isActive); + } + return Stack( + clipBehavior: Clip.none, + children: [ + if (_showTip && value == e) + Positioned( + top: -TDTheme.of(context).spacer48 / 2 + _indexSize / 2, + left: -TDTheme.of(context).spacer48, + child: Container( + height: TDTheme.of(context).spacer48, + width: TDTheme.of(context).spacer48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(TDTheme.of(context).radiusCircle), + color: TDTheme.of(context).brandColor1, + ), + child: Center( + child: TDText( + e, + forceVerticalCenter: true, + font: TDTheme.of(context).fontTitleExtraLarge, + textColor: TDTheme.of(context).brandColor7, + ), + ), + ), + ), + Container( + key: _containerKeys[e], + padding: EdgeInsets.only(left: TDTheme.of(context).spacer8), + color: Colors.transparent, + child: Container( + width: _indexSize, + height: _indexSize, + decoration: isActive + ? BoxDecoration( + borderRadius: BorderRadius.circular(TDTheme.of(context).radiusCircle), + color: TDTheme.of(context).brandColor7, + ) + : null, + child: Center( + child: TDText( + e, + forceVerticalCenter: true, + font: isActive ? TDTheme.of(context).fontMarkSmall : TDTheme.of(context).fontLinkSmall, + textColor: + isActive ? TDTheme.of(context).fontWhColor1 : TDTheme.of(context).fontGyColor1, + ), + ), + ), + ), + ], + ); + }, + ).toList(), + ); + }, + ), + ), + ), + ), + ), + ); + } + + void _changeSelect(Offset globalPosition) { + final newIndex = _fingerInsideContainer(globalPosition); + if (newIndex != null && newIndex != widget.activeIndex.value) { + final oldIndex = widget.activeIndex.value; + widget.activeIndex.value = newIndex; + _showTip = true; + widget.onSelect.call(newIndex, oldIndex); + } + } + + String? _fingerInsideContainer(Offset globalPosition) { + for (var entry in _containerKeys.entries) { + final renderBox = entry.value.currentContext?.findRenderObject() as RenderBox?; + if (renderBox != null) { + final localPosition = renderBox.globalToLocal(globalPosition); + final isIn = renderBox.hitTest(BoxHitTestResult(), position: localPosition); + if (isIn) { + return entry.key; + } + } + } + return null; + } + + void _hideTip() { + Future.delayed( + const Duration(seconds: 1), + () { + setState(() { + _showTip = false; + }); + }, + ); + } +} diff --git a/tdesign-component/lib/src/components/input/input_view.dart b/tdesign-component/lib/src/components/input/input_view.dart index a645972e0..be8205cb4 100644 --- a/tdesign-component/lib/src/components/input/input_view.dart +++ b/tdesign-component/lib/src/components/input/input_view.dart @@ -70,6 +70,9 @@ class TDInputView extends StatelessWidget { /// 键盘动作类型 final TextInputAction? inputAction; + /// 点击输入框外部区域回调 + final TapRegionCallback? onTapOutside; + const TDInputView( {Key? key, required this.textStyle, @@ -94,7 +97,8 @@ class TDInputView extends StatelessWidget { this.isCollapsed = false, this.textAlign, this.controller, - this.inputAction,}) + this.inputAction, + this.onTapOutside}) : super( key: key, ); @@ -117,6 +121,7 @@ class TDInputView extends StatelessWidget { maxLines: maxLines, minLines: minLines, maxLength: maxLength, + onTapOutside: onTapOutside, style: textStyle, textAlign: textAlign ?? TextAlign.start, buildCounter: _buildCounter, diff --git a/tdesign-component/lib/src/components/input/td_input.dart b/tdesign-component/lib/src/components/input/td_input.dart index 5c71f423a..a24893103 100644 --- a/tdesign-component/lib/src/components/input/td_input.dart +++ b/tdesign-component/lib/src/components/input/td_input.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -60,7 +62,8 @@ class TDInput extends StatelessWidget { this.cardStyleTopText, this.inputAction, TDInputSpacer? spacer, - this.cardStyleBottomText}) + this.cardStyleBottomText, + this.onTapOutside}) : // assert(() { // if (type == TDInputType.cardStyle) { @@ -225,7 +228,7 @@ class TDInput extends StatelessWidget { /// 内容对齐方向 final TextAlign contentAlignment; - /// 左侧标签样式 + /// 左侧标签样式 设置该值是若出现像素溢出,请设置letterSpacing: 0 final TextStyle? leftLabelStyle; /// 键盘动作类型 @@ -237,6 +240,9 @@ class TDInput extends StatelessWidget { /// 左侧内容所占区域宽度 double _leftLabelWidth = 0; + /// 点击输入框外部区域回调 + final TapRegionCallback? onTapOutside; + /// 获取输入框规格 double getInputPadding() { switch (size) { @@ -284,46 +290,52 @@ class TDInput extends StatelessWidget { width: leftLabelSpace ?? 16, ), ), - Row( - children: [ - Visibility( - visible: leftIcon != null, - child: leftIcon ?? const SizedBox.shrink(), - ), - Visibility( - visible: leftLabel != null, - child: Container( - constraints: BoxConstraints(maxWidth: _leftLabelWidth), - padding: EdgeInsets.only( - left: leftIcon != null ? spacer.iconLabelSpace! : 0, - top: getInputPadding(), - bottom: getInputPadding()), - child: TDText( - leftLabel, - maxLines: 2, - style: leftLabelStyle, - font: TDTheme.of(context).fontBodyLarge, - fontWeight: FontWeight.w400, + SizedBox( + width: _leftLabelWidth, + child: Row( + children: [ + Visibility( + visible: leftIcon != null, + child: SizedBox( + width: 24, + child: leftIcon ?? const SizedBox.shrink(), ), ), - ), - Visibility( - visible: labelWidget != null, - child: labelWidget ?? const SizedBox.shrink(), - ), - Visibility( - visible: required ?? false, - child: Padding( - padding: const EdgeInsets.only(left: 4.0), + Visibility( + visible: leftLabel != null, + child: Container( + constraints: BoxConstraints(maxWidth: _leftLabelWidth), + padding: EdgeInsets.only( + left: leftIcon != null ? (spacer.iconLabelSpace ?? 4) : 0, + top: getInputPadding(), + bottom: getInputPadding()), child: TDText( - '*', - maxLines: 1, - style: TextStyle(color: TDTheme.of(context).errorColor6), + leftLabel, + maxLines: 2, + style: leftLabelStyle ?? const TextStyle(letterSpacing: 0), font: TDTheme.of(context).fontBodyLarge, fontWeight: FontWeight.w400, ), - )), - ], + ), + ), + Visibility( + visible: labelWidget != null, + child: labelWidget ?? const SizedBox.shrink(), + ), + Visibility( + visible: required ?? false, + child: Padding( + padding: const EdgeInsets.only(left: 4.0), + child: TDText( + '*', + maxLines: 1, + style: TextStyle(color: TDTheme.of(context).errorColor6), + font: TDTheme.of(context).fontBodyLarge, + fontWeight: FontWeight.w400, + ), + )), + ], + ), ), Expanded( flex: 1, @@ -340,6 +352,7 @@ class TDInput extends StatelessWidget { hintText: hintText, inputType: inputType, onChanged: onChanged, + onTapOutside: onTapOutside, inputFormatters: inputFormatters, inputDecoration: inputDecoration, maxLines: maxLines, @@ -353,15 +366,15 @@ class TDInput extends StatelessWidget { controller: controller, contentPadding: contentPadding ?? EdgeInsets.only( - left: spacer.labelInputSpace!, - right: spacer.inputRightSpace! / 2, + left: spacer.labelInputSpace ?? 16, + right: spacer.inputRightSpace != null ? spacer.inputRightSpace! / 2 : 16, bottom: additionInfo != '' ? 4 : getInputPadding(), top: getInputPadding()), inputAction: inputAction, ), Visibility( child: Padding( - padding: EdgeInsets.only(left: spacer.additionInfoSpace!, bottom: getInputPadding()), + padding: EdgeInsets.only(left: spacer.additionInfoSpace ?? 16, bottom: getInputPadding()), child: TDText( additionInfo, font: TDTheme.of(context).fontBodySmall, @@ -385,8 +398,8 @@ class TDInput extends StatelessWidget { child: GestureDetector( child: Container( margin: EdgeInsets.only( - left: spacer.inputRightSpace! / 2, - right: spacer.rightSpace!, + left: spacer.inputRightSpace != null ? spacer.inputRightSpace! / 2 : 8, + right: spacer.rightSpace ?? 16, top: additionInfo != '' ? getInputPadding() : 0), child: Icon( size: clearIconSize, @@ -404,8 +417,8 @@ class TDInput extends StatelessWidget { onTap: onBtnTap, child: Container( margin: EdgeInsets.only( - left: spacer.inputRightSpace! / 2, - right: spacer.rightSpace!, + left: spacer.inputRightSpace != null ? spacer.inputRightSpace! / 2 : 8, + right: spacer.rightSpace ?? 16, top: additionInfo != '' ? getInputPadding() : 0), child: rightBtn, ), @@ -483,7 +496,7 @@ class TDInput extends StatelessWidget { TDText( leftLabel, maxLines: 2, - style: leftLabelStyle, + style: leftLabelStyle ?? const TextStyle(letterSpacing: 0), font: TDTheme.of(context).fontBodyLarge, fontWeight: FontWeight.w400, ), @@ -543,7 +556,10 @@ class TDInput extends StatelessWidget { textInputBackgroundColor: textInputBackgroundColor, controller: controller, contentPadding: contentPadding ?? - EdgeInsets.only(left: spacer.labelInputSpace!, right: spacer.inputRightSpace! / 2), + EdgeInsets.only( + left: spacer.labelInputSpace ?? 16, + right: spacer.inputRightSpace != null ? spacer.inputRightSpace! / 2 : 8, + ), inputAction: inputAction, ), ), @@ -551,7 +567,10 @@ class TDInput extends StatelessWidget { visible: controller != null && controller!.text.isNotEmpty && needClear, child: GestureDetector( child: Container( - margin: EdgeInsets.only(left: spacer.inputRightSpace! / 2, right: spacer.rightSpace!), + margin: EdgeInsets.only( + left: spacer.inputRightSpace != null ? spacer.inputRightSpace! / 2 : 8, + right: spacer.rightSpace ?? 16, + ), child: Icon( size: clearIconSize, TDIcons.close_circle_filled, @@ -565,7 +584,10 @@ class TDInput extends StatelessWidget { child: GestureDetector( onTap: onBtnTap, child: Container( - margin: EdgeInsets.only(left: spacer.inputRightSpace! / 2, right: spacer.rightSpace!), + margin: EdgeInsets.only( + left: spacer.inputRightSpace != null ? spacer.inputRightSpace! / 2 : 8, + right: spacer.rightSpace ?? 16, + ), child: rightBtn, ), ), diff --git a/tdesign-component/lib/src/components/navbar/td_nav_bar.dart b/tdesign-component/lib/src/components/navbar/td_nav_bar.dart index 9bca86164..f27d9bb4b 100644 --- a/tdesign-component/lib/src/components/navbar/td_nav_bar.dart +++ b/tdesign-component/lib/src/components/navbar/td_nav_bar.dart @@ -27,6 +27,7 @@ class TDNavBar extends StatefulWidget implements PreferredSizeWidget { this.useBorderStyle = false, this.border, this.belowTitleWidget, + this.boxShadow, }) : super(key: key); /// 左边操作项 @@ -77,6 +78,9 @@ class TDNavBar extends StatefulWidget implements PreferredSizeWidget { /// belowTitleWidget navbar 下方的widget final Widget? belowTitleWidget; + /// 底部阴影 + final List? boxShadow; + @override State createState() => _TDNavBarState(); @@ -222,9 +226,12 @@ class _TDNavBarState extends State { print("screenAdaptation:${widget.screenAdaptation}, paddingTop:$paddingTop"); return Container( - color: bcc, height: widget.height + paddingTop, padding: padding.add(EdgeInsets.only(top: paddingTop)), + decoration: BoxDecoration( + color: bcc, + boxShadow: widget.boxShadow, + ), child: _getNavbarChild() ); } diff --git a/tdesign-component/lib/src/components/notice_bar/td_notice_bar.dart b/tdesign-component/lib/src/components/notice_bar/td_notice_bar.dart new file mode 100644 index 000000000..591caa303 --- /dev/null +++ b/tdesign-component/lib/src/components/notice_bar/td_notice_bar.dart @@ -0,0 +1,406 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import '../../../tdesign_flutter.dart'; + +class TDNoticeBar extends StatefulWidget { + const TDNoticeBar({ + super.key, + this.context, + this.style, + this.left, + this.right, + this.speed = 50, + this.interval = 3000, + this.marquee = false, + this.direction = Axis.horizontal, + this.theme = TDNoticeBarTheme.info, + this.prefixIcon, + this.suffixIcon, + this.onTap, + this.height = 22, + }); + + /// 文本内容 + final dynamic context; + + /// 公告栏样式 + final TDNoticeBarStyle? style; + + /// 左侧内容(自定义左侧内容,优先级高于prefixIcon) + final Widget? left; + + /// 右侧内容(自定义右侧内容,优先级高于suffixIcon) + final Widget? right; + + /// 跑马灯效果 + final bool? marquee; + + /// 滚动速度 + final double? speed; + + /// 步进滚动间隔时间(毫秒) + final int? interval; + + /// 滚动方向 + final Axis? direction; + + /// 主题 + final TDNoticeBarTheme? theme; + + /// 左侧图标 + final IconData? prefixIcon; + + /// 右侧图标 + final IconData? suffixIcon; + + /// 点击事件 + final ValueChanged? onTap; + + /// 文字高度 (当使用prefixIcon或suffixIcon时,icon大小值等于该属性) + final double height; + + @override + State createState() => _TDNoticeBarState(); +} + +class _TDNoticeBarState extends State { + ScrollController? _scrollController; + Timer? _timer; + Size? _size; + TDNoticeBarStyle? _style; + Color? _backgroundColor; + Widget? _left; + Widget? _right; + final GlobalKey _key = GlobalKey(); + final GlobalKey _contextKey = GlobalKey(); + + @override + void initState() { + super.initState(); + if (widget.speed! < 0) { + throw Exception('speed must not be less than 0'); + } + if (widget.interval! <= 0) { + throw Exception('interval must not be less than 0'); + } + _scrollController = ScrollController(); + _init(); + WidgetsBinding.instance.addPostFrameCallback((time) { + if (widget.marquee == true) { + _startTimer(); + } + }); + } + + @override + void dispose() { + super.dispose(); + _timer?.cancel(); + _scrollController?.dispose(); + } + + void _init() { + if (widget.style != null) { + _style = widget.style; + } else { + _style = TDNoticeBarStyle.generateTheme(context, theme: widget.theme); + } + _backgroundColor = _style!.backgroundColor; + _setLeftWidget(); + _setRightWidget(); + } + + void _startTimer() { + if (widget.direction == Axis.horizontal) { + _scroll(); + } else if (widget.direction == Axis.vertical) { + _step(); + } + } + + void _scroll() { + var scrollDistance = + _getContextWidth() + (_size!.width - _style!.getPadding.horizontal); + var remainder = scrollDistance % widget.speed!; + _scrollController!.jumpTo(0); + var offset = 0.0 + widget.speed!; + _scrollController!.animateTo(offset, + duration: const Duration(seconds: 1), curve: Curves.linear); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) async { + if (offset < scrollDistance - remainder) { + offset += widget.speed!; + await _scrollController!.animateTo(offset, + duration: const Duration(seconds: 1), curve: Curves.linear); + } else { + // 剩余距离小于50 先滚动这部分 然后滚动剩余部分 + // 剩余距离滚动所需时间 + var time = (remainder / widget.speed! * 1000).round(); + // 滚动最后一部分(触底) + await _scrollController!.animateTo(scrollDistance, + duration: Duration(milliseconds: time), curve: Curves.linear); + // 回到顶部(衔接) + _scrollController!.jumpTo(0); + // 修改起始位置 + offset = widget.speed! - remainder; + // 计算新起点最后阶段滚动距离 + remainder = (scrollDistance - offset) % widget.speed!; + // 滚动至新起点(弥补触底speed滚动长度) + await _scrollController!.animateTo(offset, + duration: Duration(milliseconds: 1000 - time), + curve: Curves.linear); + } + }); + } + + void _step() { + var step = 0; + var offset = 0.0; + _timer = Timer.periodic(Duration(milliseconds: widget.interval!), (timer) { + var time = (widget.height / widget.speed! * 1000).round(); + if (step >= widget.context.length) { + step = 0; + offset = 0; + _scrollController!.jumpTo(0); + } + step++; + // 固定滚动行高(22) + offset += widget.height; + _scrollController!.animateTo(offset, + duration: Duration(milliseconds: time), curve: Curves.linear); + }); + } + + /// 获取文本内容尺寸消息 + Size _getFontSize() { + var text = widget.context; + if (widget.context is List) { + text = widget.context[0]; + } + final textPainter = TextPainter( + text: TextSpan( + text: text, + style: _style!.getTextStyle, + ), + locale: Localizations.localeOf(context), + textDirection: TextDirection.ltr, + maxLines: 1, + )..layout(maxWidth: _size!.width); + return textPainter.size; + } + + /// 设置左侧内容 + void _setLeftWidget() { + if (widget.prefixIcon != null) { + _left = Icon( + widget.prefixIcon, + color: _style!.leftIconColor, + size: widget.height, + ); + } + if (widget.left != null) { + _left = widget.left; + } + } + + /// 设置右侧内容 + void _setRightWidget() { + if (widget.suffixIcon != null) { + _right = Icon( + widget.suffixIcon, + color: _style!.rightIconColor, + size: widget.height, + ); + } + if (widget.right != null) { + _right = widget.right; + } + } + + /// 获取文本内容宽度 + double _getContextWidth() { + var contextWidth = + _key.currentContext?.findRenderObject()?.paintBounds.size.width ?? 0; + if (contextWidth == 0) { + contextWidth = _getFontSize().width; + } + return contextWidth; + } + + /// 获取滚动区域宽度 + double _getEmptyWidth() { + return _contextKey.currentContext + ?.findRenderObject() + ?.paintBounds + .size + .width ?? + (_size!.width - _style!.getPadding.horizontal); + } + + /// 内容区域 + Widget _contextWidget() { + var valid = false; + Widget? textWidget; + if (widget.context is String) { + valid = true; + textWidget = SizedBox( + height: widget.height, + child: Align( + alignment: Alignment.centerLeft, + child: TDText( + widget.context, + style: _style?.getTextStyle, + maxLines: 1, + forceVerticalCenter: true, + ), + ), + ); + } + if (widget.context is List) { + valid = true; + textWidget = SizedBox( + height: widget.height, + child: Align( + alignment: Alignment.centerLeft, + child: TDText( + widget.context[0], + style: _style?.getTextStyle, + maxLines: 1, + forceVerticalCenter: true, + ), + ), + ); + } + if (!valid) { + throw Exception('context must be String or List'); + } + if (widget.marquee == false) { + return textWidget!; + } + Widget? child; + switch (widget.direction) { + case Axis.horizontal: + child = SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + child: Row( + children: [ + SizedBox( + key: _key, + height: widget.height, + child: textWidget, + ), + SizedBox(width: _getEmptyWidth()), + SizedBox( + width: _getEmptyWidth() > _getContextWidth() + ? _getEmptyWidth() + : _getContextWidth(), + height: widget.height, + child: textWidget, + ) + ], + ), + ); + break; + case Axis.vertical: + var contexts = widget.context as List; + child = SizedBox( + height: widget.height, + child: SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.vertical, + // physics: const NeverScrollableScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < contexts.length; i++) + SizedBox( + height: widget.height, + child: Align( + alignment: Alignment.centerLeft, + child: TDText( + contexts[i], + style: _style!.getTextStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + SizedBox( + key: _key, + height: widget.height, + child: Align( + alignment: Alignment.centerLeft, + child: TDText( + contexts[0], + style: _style?.getTextStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ]), + ), + ); + break; + default: + child = textWidget; + break; + } + return child!; + } + + void _onTap(trigger) { + if (widget.onTap != null) { + widget.onTap!(trigger); + } + } + + @override + Widget build(BuildContext context) { + _size = MediaQuery.of(context).size; + return Container( + padding: _style!.getPadding, + decoration: BoxDecoration( + color: _backgroundColor, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Visibility( + visible: _left != null, + child: GestureDetector( + onTap: () => _onTap('prefix-icon'), + child: Container( + margin: const EdgeInsets.only(right: 8), + child: _left, + ), + ), + ), + Expanded( + key: _contextKey, + child: GestureDetector( + onTap: () => _onTap('context'), + child: _contextWidget(), + ), + ), + Visibility( + visible: _right != null, + child: GestureDetector( + onTap: () => _onTap('suffix-icon'), + child: _right, + ), + ), + ], + ), + ); + } +} diff --git a/tdesign-component/lib/src/components/notice_bar/td_notice_bar_style.dart b/tdesign-component/lib/src/components/notice_bar/td_notice_bar_style.dart new file mode 100644 index 000000000..b08c4e184 --- /dev/null +++ b/tdesign-component/lib/src/components/notice_bar/td_notice_bar_style.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; + +import '../../theme/td_colors.dart'; +import '../../theme/td_theme.dart'; +import 'td_notice_bar.dart'; + +/// 公告栏类型 +enum TDNoticeBarType { + /// 静止(默认) + none, + + /// 滚动 + scroll, + + /// 步进 + step +} + +/// 公告栏主题 +enum TDNoticeBarTheme { + /// 默认 + info, + + /// 成功 + success, + + /// 警告 + warning, + + /// 错误 + error +} + +/// 公告栏样式 +class TDNoticeBarStyle { + TDNoticeBarStyle( + {this.context, + this.backgroundColor, + this.textStyle, + this.leftIconColor, + this.rightIconColor, + this.padding}); + + /// 上下文 + BuildContext? context; + + /// 公告栏背景色 + Color? backgroundColor; + + /// 公告栏左侧图标颜色 + Color? leftIconColor; + + /// 公告栏右侧图标颜色 + Color? rightIconColor; + + /// 公告栏内边距 + EdgeInsetsGeometry? padding; + + /// 公告栏内容样式 + TextStyle? textStyle; + + /// 公告栏内边距,用于获取默认值 + EdgeInsetsGeometry get getPadding => + padding ?? + const EdgeInsets.only(top: 13, bottom: 13, left: 16, right: 12); + + /// 公告栏内容样式,用于获取默认值 + TextStyle get getTextStyle => + textStyle ?? + TextStyle( + color: TDTheme.of(context).fontGyColor1, + fontSize: 14, + height: 1, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ); + + /// 根据主题生成样式 + TDNoticeBarStyle.generateTheme(BuildContext context, + {TDNoticeBarTheme? theme = TDNoticeBarTheme.info}) { + rightIconColor = TDTheme.of(context).grayColor7; + switch (theme) { + case TDNoticeBarTheme.warning: + leftIconColor = TDTheme.of(context).warningNormalColor; + backgroundColor = TDTheme.of(context).warningLightColor; + break; + case TDNoticeBarTheme.error: + leftIconColor = TDTheme.of(context).errorNormalColor; + backgroundColor = TDTheme.of(context).errorLightColor; + break; + case TDNoticeBarTheme.success: + leftIconColor = TDTheme.of(context).successNormalColor; + backgroundColor = TDTheme.of(context).successLightColor; + break; + default: + leftIconColor = TDTheme.of(context).brandNormalColor; + backgroundColor = TDTheme.of(context).brandLightColor; + break; + } + } +} diff --git a/tdesign-component/lib/src/components/picker/td_date_picker.dart b/tdesign-component/lib/src/components/picker/td_date_picker.dart index ddf90df47..c469d41ec 100644 --- a/tdesign-component/lib/src/components/picker/td_date_picker.dart +++ b/tdesign-component/lib/src/components/picker/td_date_picker.dart @@ -33,6 +33,7 @@ class TDDatePicker extends StatefulWidget { this.showTitle = true, this.pickerHeight = 200, required this.pickerItemCount, + this.onSelectedItemChanged, Key? key}) : super(key: key); @@ -99,6 +100,9 @@ class TDDatePicker extends StatefulWidget { /// 数据模型 final DatePickerModel model; + /// 选择器选中项改变回调 + final void Function(int index)? onSelectedItemChanged; + @override State createState() => _TDDatePickerState(); } @@ -113,6 +117,12 @@ class _TDDatePickerState extends State { pickerHeight = widget.pickerHeight; } + @override + void dispose() { + widget.model.removeListener(); + super.dispose(); + } + bool useAll() { if(widget.model.useYear && widget.model.useMonth @@ -264,6 +274,7 @@ class _TDDatePickerState extends State { pickerHeight = pickerHeight - Random().nextDouble() / 100000000; }); } + widget.onSelectedItemChanged?.call(index); }, childDelegate: ListWheelChildBuilderDelegate( childCount: widget.model.data[whichLine].length, @@ -289,7 +300,14 @@ class _TDDatePickerState extends State { Widget buildTitle(BuildContext context) { return Container( padding: EdgeInsets.only(left: widget.leftPadding ?? 16, right: widget.rightPadding ?? 16), - + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 1, + color: widget.titleDividerColor ?? Colors.transparent, + ), + ) + ), /// 减去分割线的空间 height: getTitleHeight() - 0.5, child: Row( @@ -525,20 +543,36 @@ class DatePickerModel { secondFixedExtentScrollController = controllers[6]; } + void _yearListener() { + yearIndex = yearFixedExtentScrollController.selectedItem; + } + + void _monthListener() { + monthIndex = monthFixedExtentScrollController.selectedItem; + } + + void _dayListener() { + dayIndex = dayFixedExtentScrollController.selectedItem; + } + + void _weekDayListener() { + weekDayIndex = weekDayFixedExtentScrollController.selectedItem; + } + void addListener() { /// 给年月日加上监控 - yearFixedExtentScrollController.addListener(() { - yearIndex = yearFixedExtentScrollController.selectedItem; - }); - monthFixedExtentScrollController.addListener(() { - monthIndex = monthFixedExtentScrollController.selectedItem; - }); - dayFixedExtentScrollController.addListener(() { - dayIndex = dayFixedExtentScrollController.selectedItem; - }); - weekDayFixedExtentScrollController.addListener(() { - weekDayIndex = weekDayFixedExtentScrollController.selectedItem; - }); + yearFixedExtentScrollController.addListener(_yearListener); + monthFixedExtentScrollController.addListener(_monthListener); + dayFixedExtentScrollController.addListener(_dayListener); + weekDayFixedExtentScrollController.addListener(_weekDayListener); + } + + void removeListener() { + /// 移除年月日的监控 + yearFixedExtentScrollController.removeListener(_yearListener); + monthFixedExtentScrollController.removeListener(_monthListener); + dayFixedExtentScrollController.removeListener(_dayListener); + weekDayFixedExtentScrollController.removeListener(_weekDayListener); } void refreshMonthDataAndController() { diff --git a/tdesign-component/lib/src/components/picker/td_multi_picker.dart b/tdesign-component/lib/src/components/picker/td_multi_picker.dart index 45275416e..fd3243249 100644 --- a/tdesign-component/lib/src/components/picker/td_multi_picker.dart +++ b/tdesign-component/lib/src/components/picker/td_multi_picker.dart @@ -31,6 +31,12 @@ class TDMultiPicker extends StatelessWidget { /// 自定义选择框样式 final Widget? customSelectWidget; + /// 右侧按钮文案 + final String? rightText; + + /// 左侧按钮文案 + final String? leftText; + /// 自定义左侧文案样式 final TextStyle? leftTextStyle; @@ -80,6 +86,8 @@ class TDMultiPicker extends StatelessWidget { required this.pickerHeight, required this.pickerItemCount, this.initialIndexes, + this.rightText, + this.leftText, this.leftTextStyle, this.rightTextStyle, this.centerTextStyle, @@ -189,8 +197,19 @@ class TDMultiPicker extends StatelessWidget { Widget buildTitle(BuildContext context, List controllers) { return Container( - padding: - EdgeInsets.only(left: leftPadding ?? 16, right: rightPadding ?? 16), + padding: EdgeInsets.only( + left: leftPadding ?? 16, + right: rightPadding ?? 16, + top: topPadding ?? 16, + ), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 0.5, + color: titleDividerColor ?? Colors.transparent, + ) + ), + ), height: getTitleHeight(), child: Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -209,7 +228,7 @@ class TDMultiPicker extends StatelessWidget { }, behavior: HitTestBehavior.opaque, child: TDText( - context.resource.cancel, + leftText ?? context.resource.cancel, style: leftTextStyle?? TextStyle( fontSize: TDTheme.of(context).fontBodyLarge!.size, color: TDTheme.of(context).fontGyColor2 @@ -244,7 +263,7 @@ class TDMultiPicker extends StatelessWidget { }, behavior: HitTestBehavior.opaque, child: TDText( - context.resource.confirm, + rightText ?? context.resource.confirm, style: rightTextStyle?? TextStyle( fontSize: TDTheme.of(context).fontBodyLarge!.size, color: TDTheme.of(context).brandNormalColor @@ -318,6 +337,12 @@ class TDMultiLinkedPicker extends StatefulWidget { /// 自定义选择框样式 final Widget? customSelectWidget; + /// 右侧按钮文案 + final String? rightText; + + /// 左侧按钮文案 + final String? leftText; + /// 自定义左侧文案样式 final TextStyle? leftTextStyle; @@ -364,6 +389,8 @@ class TDMultiLinkedPicker extends StatefulWidget { this.pickerHeight = 200, this.pickerItemCount = 5, this.customSelectWidget, + this.rightText, + this.leftText, this.leftTextStyle, this.rightTextStyle, this.centerTextStyle, @@ -534,7 +561,18 @@ class _TDMultiLinkedPickerState extends State { Widget buildTitle(BuildContext context) { return Container( padding: EdgeInsets.only( - left: widget.leftPadding ?? 16, right: widget.rightPadding ?? 16), + left: widget.leftPadding ?? 16, + right: widget.rightPadding ?? 16, + top: widget.topPadding ?? 16, + ), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 0.5, + color: widget.titleDividerColor ?? Colors.transparent, + ) + ) + ), height: getTitleHeight() - 0.5, child: Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -550,7 +588,7 @@ class _TDMultiLinkedPickerState extends State { }, behavior: HitTestBehavior.opaque, child: TDText( - context.resource.cancel, + widget.leftText ?? context.resource.cancel, style: widget.leftTextStyle ?? TextStyle( fontSize: TDTheme.of(context).fontBodyLarge!.size, color: TDTheme.of(context).fontGyColor2, @@ -582,7 +620,7 @@ class _TDMultiLinkedPickerState extends State { }, behavior: HitTestBehavior.opaque, child: TDText( - context.resource.confirm, + widget.rightText ?? context.resource.confirm, style: widget.rightTextStyle ?? TextStyle( fontSize: TDTheme.of(context).fontBodyLarge!.size, color: TDTheme.of(context).brandNormalColor, diff --git a/tdesign-component/lib/src/components/picker/td_picker.dart b/tdesign-component/lib/src/components/picker/td_picker.dart index 08f6ed75c..b44f8e4d5 100644 --- a/tdesign-component/lib/src/components/picker/td_picker.dart +++ b/tdesign-component/lib/src/components/picker/td_picker.dart @@ -22,6 +22,10 @@ class TDPicker { List? initialDate, String? rightText, String? leftText, + TextStyle? leftTextStyle, + TextStyle? centerTextStyle, + TextStyle? rightTextStyle, + Color? titleDividerColor, Duration duration = const Duration(milliseconds: 100), double pickerHeight = 200, int pickerItemCount = 5}) { @@ -42,6 +46,10 @@ class TDPicker { onCancel: onCancel, rightText: rightText, leftText: leftText, + leftTextStyle: leftTextStyle, + centerTextStyle: centerTextStyle, + rightTextStyle: rightTextStyle, + titleDividerColor: titleDividerColor, model: DatePickerModel( useYear: useYear, useMonth: useMonth, @@ -69,6 +77,13 @@ class TDPicker { Duration duration = const Duration(milliseconds: 100), Color? barrierColor, double pickerHeight = 200, + String? rightText, + String? leftText, + TextStyle? leftTextStyle, + TextStyle? centerTextStyle, + TextStyle? rightTextStyle, + Color? titleDividerColor, + double? topPadding, int pickerItemCount = 5}) { showModalBottomSheet( context: context, @@ -80,9 +95,16 @@ class TDPicker { onConfirm: onConfirm, onCancel: onCancel, data: data, + rightText: rightText, + leftText: leftText, + leftTextStyle: leftTextStyle, + centerTextStyle: centerTextStyle, + rightTextStyle: rightTextStyle, initialIndexes: initialIndexes, pickerHeight: pickerHeight, pickerItemCount: pickerItemCount, + titleDividerColor: titleDividerColor, + topPadding: topPadding, ); }); } @@ -97,7 +119,14 @@ class TDPicker { required List initialData, Duration duration = const Duration(milliseconds: 100), Color? barrierColor, + String? rightText, + String? leftText, + TextStyle? leftTextStyle, + TextStyle? centerTextStyle, + TextStyle? rightTextStyle, double pickerHeight = 200, + Color? titleDividerColor, + double? topPadding, int pickerItemCount = 5}) { showModalBottomSheet( context: context, @@ -109,10 +138,17 @@ class TDPicker { onConfirm: onConfirm, onCancel: onCancel, data: data, + rightText: rightText, + leftText: leftText, + leftTextStyle: leftTextStyle, + centerTextStyle: centerTextStyle, + rightTextStyle: rightTextStyle, pickerHeight: pickerHeight, pickerItemCount: pickerItemCount, columnNum: columnNum, selectedData: initialData, + titleDividerColor: titleDividerColor, + topPadding: topPadding, ); }); } diff --git a/tdesign-component/lib/src/components/popup/td_popup_route.dart b/tdesign-component/lib/src/components/popup/td_popup_route.dart index 6f7bbd622..92204cb10 100644 --- a/tdesign-component/lib/src/components/popup/td_popup_route.dart +++ b/tdesign-component/lib/src/components/popup/td_popup_route.dart @@ -1,4 +1,7 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; const Duration _bottomSheetEnterDuration = Duration(milliseconds: 250); const Duration _bottomSheetExitDuration = Duration(milliseconds: 200); @@ -21,6 +24,8 @@ class TDSlidePopupRoute extends PopupRoute { this.modalLeft = 0, this.open, this.opened, + this.close, + this.barrierClick, }); /// 控件构建器 @@ -56,6 +61,12 @@ class TDSlidePopupRoute extends PopupRoute { /// 打开后事件 final VoidCallback? opened; + /// 关闭前事件 + final VoidCallback? close; + + /// 蒙层点击事件,仅在[modalBarrierFull]为false时触发 + final VoidCallback? barrierClick; + Color get _barrierColor => modalBarrierColor ?? Colors.black54; @override @@ -73,6 +84,13 @@ class TDSlidePopupRoute extends PopupRoute { @override Color get barrierColor => modalBarrierFull ? _barrierColor : Colors.transparent; + /// 键盘焦点对象的Y坐标 + var _focusY = 0.0; + /// 键盘焦点对象的高度 + var _focusHeight = 0.0; + /// 键盘出现后bottom的偏移量 + var _lastBottom = 0.0; + // 实现转场动画 @override Widget buildTransitions( @@ -91,6 +109,13 @@ class TDSlidePopupRoute extends PopupRoute { color: _barrierColor.withAlpha((animValue * _barrierColor.alpha).toInt()), child: GestureDetector( onTap: () { + barrierClick?.call(); + if (isDismissible) { + Navigator.pop(context); + } + }, + onDoubleTap: () { + barrierClick?.call(); if (isDismissible) { Navigator.pop(context); } @@ -128,6 +153,7 @@ class TDSlidePopupRoute extends PopupRoute { @override TickerFuture didPush() { + startFocusListener(navigator!.context); open?.call(); animation?.addStatusListener((status) { if (status == AnimationStatus.completed) { @@ -137,9 +163,58 @@ class TDSlidePopupRoute extends PopupRoute { return super.didPush(); } + @override + void dispose() { + close?.call(); + stopFocusListener(navigator!.context); + super.dispose(); + } + + + /// 监听焦点变化 + void startFocusListener(BuildContext context) { + FocusManager.instance.addListener(_handleFocusChange); + } + + /// 停止监听焦点变化 + void stopFocusListener(BuildContext context) { + FocusManager.instance.removeListener(_handleFocusChange); + } + + void _handleFocusChange() { + // 获取当前的焦点节点 + var focusNode = FocusManager.instance.primaryFocus; + if (focusNode != null && focusNode.context != null) { + var renderObject = focusNode.context!.findRenderObject(); + if (renderObject is RenderPointerListener) { + _focusY = renderObject.localToGlobal(Offset.zero).dy; + _focusHeight = renderObject.size.height; + } + } + (focusNode?.context as Element?)?.markNeedsBuild(); + bool didPop(T? result) { + close?.call(); + return super.didPop(result); + } + Widget _getPositionWidget(BuildContext context, Widget child) { - var screenSize = MediaQuery.of(context).size; - var _modalTop = (modalTop ?? 0).clamp(0, screenSize.height).toDouble(); + var bottom = 0.0; + var mediaQuery = MediaQuery.of(context); + if (slideTransitionFrom == SlideTransitionFrom.bottom) { + bottom = mediaQuery.viewInsets.bottom; + } else { + if ((_focusY + mediaQuery.viewInsets.bottom + _focusHeight) > mediaQuery.size.height) { + bottom = -(mediaQuery.size.height - (_focusY + mediaQuery.viewInsets.bottom + _focusHeight + 10)); + _lastBottom = bottom; + } else { + if (_lastBottom > 0.0) { + bottom = max((_lastBottom -= 5), 0).toDouble(); + } + } + } + + var screenSize = mediaQuery.size; + var _modalTop = (modalTop ?? 0).clamp(0, screenSize.height).toDouble() - bottom; var _modalLeft = (modalLeft ?? 0).clamp(0, screenSize.width).toDouble(); var _modalHeight = (modalHeight ?? screenSize.height).clamp(0, screenSize.height - _modalTop).toDouble(); var _modalWidth = (modalWidth ?? screenSize.width).clamp(0, screenSize.width - _modalLeft).toDouble(); diff --git a/tdesign-component/lib/src/components/search/td_search_bar.dart b/tdesign-component/lib/src/components/search/td_search_bar.dart index 3b8e6b6ef..55c242361 100644 --- a/tdesign-component/lib/src/components/search/td_search_bar.dart +++ b/tdesign-component/lib/src/components/search/td_search_bar.dart @@ -26,11 +26,13 @@ class TDSearchBar extends StatefulWidget { const TDSearchBar({ Key? key, this.placeHolder, + this.action, this.style = TDSearchStyle.square, this.alignment = TDSearchAlignment.left, this.onTextChanged, this.onSubmitted, this.onEditComplete, + this.onActionClick, this.autoHeight = false, this.padding = const EdgeInsets.fromLTRB(16, 8, 16, 8), this.autoFocus = false, @@ -45,6 +47,9 @@ class TDSearchBar extends StatefulWidget { /// 预设文案 final String? placeHolder; + /// 右侧操作按钮文字 + final String? action; + /// 样式 final TDSearchStyle? style; @@ -86,6 +91,8 @@ class TDSearchBar extends StatefulWidget { /// 自定义操作回调 final TDSearchBarEvent? onActionClick; + /// 右侧操作按钮点击回调 + final TDSearchBarCallBack? onActionClick; @override State createState() => _TDSearchBarState(); @@ -289,10 +296,14 @@ class _TDSearchBarState extends State offstage: cancelBtnHide || !widget.needCancel, child: GestureDetector( onTap: () { + if (widget.onActionClick != null) { + widget.onActionClick!(); + } else { _cleanInputText(); if (widget.onTextChanged != null) { widget.onTextChanged!(''); } + } if (_animation == null) { focusNode.unfocus(); setState(() { @@ -316,6 +327,41 @@ class _TDSearchBarState extends State ), ), ), + Offstage( + offstage: cancelBtnHide || !widget.needCancel, + child: GestureDetector( + onTap: () { + if (widget.onActionClick != null) { + widget.onActionClick!(); + } else { + _cleanInputText(); + if (widget.onTextChanged != null) { + widget.onTextChanged!(''); + } + } + if (_animation == null) { + focusNode.unfocus(); + setState(() { + _status = _TDSearchBarStatus.unFocus; + }); + return; + } + setState(() { + _status = _TDSearchBarStatus.animatingToUnFocus; + }); + focusNode.unfocus(); + _animationController.reverse( + from: _animationController.upperBound); + }, + child: Container( + padding: const EdgeInsets.only(left: 16), + child: Text(widget.action ?? context.resource.cancel, + style: TextStyle( + fontSize: getSize(context)?.size, + color: TDTheme.of(context).brandNormalColor)), + ), + ), + ), ], ), Offstage( diff --git a/tdesign-component/lib/src/components/sidebar/td_sidebar.dart b/tdesign-component/lib/src/components/sidebar/td_sidebar.dart index 8a66f2d3d..2c714c62d 100644 --- a/tdesign-component/lib/src/components/sidebar/td_sidebar.dart +++ b/tdesign-component/lib/src/components/sidebar/td_sidebar.dart @@ -41,6 +41,8 @@ class TDSideBar extends StatefulWidget { this.contentPadding, this.selectedTextStyle, this.style = TDSideBarStyle.normal, + this.loading, + this.loadingWidget, }) : super(key: key); /// 选项值 @@ -76,6 +78,12 @@ class TDSideBar extends StatefulWidget { /// 控制器 final TDSideBarController? controller; + /// 加载效果 + final bool? loading; + + /// 自定义加载动画 + final Widget? loadingWidget; + @override State createState() => _TDSideBarState(); } @@ -87,6 +95,7 @@ class _TDSideBarState extends State { final _scrollerController = ScrollController(); final GlobalKey globalKey = GlobalKey(); final double itemHeight = 56.0; + bool _loading = false; // 查找某值对应项 SideItemProps findSideItem(int value) { @@ -130,10 +139,25 @@ class _TDSideBarState extends State { void initState() { super.initState(); + _loading = widget.loading ?? false; // controller注册事件 if (widget.controller != null) { widget.controller!.addListener(() { selectValue(widget.controller!.currentValue, needScroll: true); + _loading = widget.controller!.loading; + displayChildren = widget.controller!.children + .asMap() + .entries + .map((entry) => SideItemProps( + index: entry.key, + disabled: entry.value.disabled, + value: entry.value.value, + icon: entry.value.icon, + label: entry.value.label, + textStyle: entry.value.textStyle, + badge: entry.value.badge)) + .toList(); + setState(() {}); }); } @@ -187,6 +211,17 @@ class _TDSideBarState extends State { @override Widget build(BuildContext context) { + if(_loading) { + if(widget.loadingWidget != null) { + return widget.loadingWidget!; + } + return SizedBox( + width: MediaQuery.of(context).size.width, + child: const Align( + child: TDLoading(icon: TDLoadingIcon.circle, size: TDLoadingSize.large), + ), + ); + } return ConstrainedBox( key: globalKey, constraints: BoxConstraints( diff --git a/tdesign-component/lib/src/components/sidebar/td_sidebar_controller.dart b/tdesign-component/lib/src/components/sidebar/td_sidebar_controller.dart index e0221e625..176fa5b53 100644 --- a/tdesign-component/lib/src/components/sidebar/td_sidebar_controller.dart +++ b/tdesign-component/lib/src/components/sidebar/td_sidebar_controller.dart @@ -1,13 +1,29 @@ import 'package:flutter/material.dart'; +import '../../../tdesign_flutter.dart'; + class TDSideBarController extends ChangeNotifier { int currentValue = 0; + List children = []; + bool _loading = true; void selectTo(int value) { currentValue = value; notifyListeners(); } + void init(List data) { + children = data; + notifyListeners(); + } + + set loading(bool load) { + _loading = load; + notifyListeners(); + } + + bool get loading => _loading; + @override void dispose() { super.dispose(); diff --git a/tdesign-component/lib/src/components/sidebar/td_wrap_sidebar_item.dart b/tdesign-component/lib/src/components/sidebar/td_wrap_sidebar_item.dart index d80de7d02..bf5b46072 100644 --- a/tdesign-component/lib/src/components/sidebar/td_wrap_sidebar_item.dart +++ b/tdesign-component/lib/src/components/sidebar/td_wrap_sidebar_item.dart @@ -179,6 +179,7 @@ class TDWrapSideBarItem extends StatelessWidget { ], ), softWrap: true, + style: selectedTextStyle, ); } diff --git a/tdesign-component/lib/src/components/slider/td_slider.dart b/tdesign-component/lib/src/components/slider/td_slider.dart index 750fdc112..bac7c899d 100644 --- a/tdesign-component/lib/src/components/slider/td_slider.dart +++ b/tdesign-component/lib/src/components/slider/td_slider.dart @@ -57,6 +57,12 @@ class TDSliderState extends State { value = widget.value; } + @override + void didUpdateWidget(covariant TDSlider oldWidget) { + super.didUpdateWidget(oldWidget); + value = widget.value; + } + bool get enabled => widget.onChanged != null; TextStyle get labelTextStyle => @@ -177,6 +183,12 @@ class _TDRangeSliderState extends State { rangeValues = widget.value; } + @override + void didUpdateWidget(covariant TDRangeSlider oldWidget) { + super.didUpdateWidget(oldWidget); + rangeValues = widget.value; + } + bool get enabled => widget.onChanged != null; TextStyle get labelTextStyle => diff --git a/tdesign-component/lib/src/components/tabs/td_horizontal_tab_bar.dart b/tdesign-component/lib/src/components/tabs/td_horizontal_tab_bar.dart index 0a56ec4b7..a606b722c 100644 --- a/tdesign-component/lib/src/components/tabs/td_horizontal_tab_bar.dart +++ b/tdesign-component/lib/src/components/tabs/td_horizontal_tab_bar.dart @@ -14,7 +14,7 @@ import '../../../tdesign_flutter.dart'; const double _kTabHeight = 46.0; const double _kTextAndIconTabHeight = 72.0; - +const double _kStartOffset = 52.0; class _TabStyle extends AnimatedWidget { const _TabStyle({ Key? key, @@ -127,6 +127,7 @@ class TDHorizontalTabBar extends StatefulWidget implements PreferredSizeWidget { this.backgroundColor, this.selectedBgColor, this.unSelectedBgColor, + this.tabAlignment }) : assert(indicator != null || (indicatorWeight > 0.0)), super(key: key); @@ -323,6 +324,7 @@ class TDHorizontalTabBar extends StatefulWidget implements PreferredSizeWidget { /// 未选中背景色 final Color? unSelectedBgColor; + final TabAlignment? tabAlignment; /// A size whose height depends on if the tabs have both icons and text. /// /// [AppBar] uses this size to compute its own preferred size. @@ -823,9 +825,32 @@ class _TDHorizontalTabBarState extends State { } return null; } - + TabAlignment get _defaults { + return widget.isScrollable ? TabAlignment.start : TabAlignment.fill; + } + bool _debugTabAlignmentIsValid(TabAlignment tabAlignment) { + assert(() { + if (widget.isScrollable && tabAlignment == TabAlignment.fill) { + throw FlutterError( + '$tabAlignment is only valid for non-scrollable tab bars.', + ); + } + if (!widget.isScrollable + && (tabAlignment == TabAlignment.start + || tabAlignment == TabAlignment.startOffset)) { + throw FlutterError( + '$tabAlignment is only valid for scrollable tab bars.', + ); + } + return true; + }()); + return true; + } @override Widget build(BuildContext context) { + final tabBarTheme = TabBarTheme.of(context); + final TabAlignment effectiveTabAlignment = widget.tabAlignment ?? tabBarTheme.tabAlignment ?? _defaults; + assert(_debugTabAlignmentIsValid(effectiveTabAlignment)); assert(debugCheckHasMaterialLocalizations(context)); assert(() { if (_controller!.length != widget.tabs.length) { @@ -843,7 +868,7 @@ class _TDHorizontalTabBarState extends State { ); } - final tabBarTheme = TabBarTheme.of(context); + final wrappedTabs = List.generate(widget.tabs.length, (int index) { const verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight) / 2.0; @@ -961,7 +986,7 @@ class _TDHorizontalTabBarState extends State { ), ), ); - if (!widget.isScrollable) { + if (!widget.isScrollable && effectiveTabAlignment == TabAlignment.fill) { wrappedTabs[index] = Expanded(child: wrappedTabs[index]); } } @@ -977,18 +1002,21 @@ class _TDHorizontalTabBarState extends State { unselectedLabelStyle: widget.unselectedLabelStyle, child: _TabLabelBar( onPerformLayout: _saveTabOffsets, + mainAxisSize: effectiveTabAlignment == TabAlignment.fill ? MainAxisSize.max : MainAxisSize.min, children: wrappedTabs, ), ), ); if (widget.isScrollable) { + final EdgeInsetsGeometry? effectivePadding = effectiveTabAlignment == TabAlignment.startOffset + ? const EdgeInsetsDirectional.only(start: _kStartOffset).add(widget.padding ?? EdgeInsets.zero): widget.padding; _scrollController ??= _TabBarScrollController(this); tdHorizontalTabBar = SingleChildScrollView( dragStartBehavior: widget.dragStartBehavior, scrollDirection: Axis.horizontal, controller: _scrollController, - padding: widget.padding, + padding: effectivePadding, physics: widget.physics, child: tdHorizontalTabBar, ); @@ -1125,11 +1153,11 @@ class _TabLabelBar extends Flex { Key? key, List children = const [], required this.onPerformLayout, + required super.mainAxisSize, }) : super( key: key, children: children, direction: Axis.horizontal, - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, verticalDirection: VerticalDirection.down, diff --git a/tdesign-component/lib/src/components/tabs/td_tab_bar.dart b/tdesign-component/lib/src/components/tabs/td_tab_bar.dart index ce3ec6a49..c47530bfe 100644 --- a/tdesign-component/lib/src/components/tabs/td_tab_bar.dart +++ b/tdesign-component/lib/src/components/tabs/td_tab_bar.dart @@ -42,6 +42,7 @@ class TDTabBar extends StatefulWidget { this.dividerHeight = 0.5, this.selectedBgColor, this.unSelectedBgColor, + this.tabAlignment, }) : assert( backgroundColor == null || decoration == null, 'Cannot provide both a backgroundColor and a decoration\n' @@ -124,6 +125,7 @@ class TDTabBar extends StatefulWidget { /// 未选中背景色,只有outlineType为capsule时有效 final Color? unSelectedBgColor; + final TabAlignment? tabAlignment; @override State createState() => _TDTabBarState(); } @@ -165,6 +167,7 @@ class _TDTabBarState extends State { backgroundColor: widget.backgroundColor, selectedBgColor: widget.selectedBgColor, unSelectedBgColor: widget.unSelectedBgColor, + tabAlignment:widget.tabAlignment, onTap: (index) { widget.onTap?.call(index); }, diff --git a/tdesign-component/lib/src/components/count_down/td_count_down.dart b/tdesign-component/lib/src/components/time_counter/td_time_counter.dart similarity index 51% rename from tdesign-component/lib/src/components/count_down/td_count_down.dart rename to tdesign-component/lib/src/components/time_counter/td_time_counter.dart index 806abcb43..196364113 100644 --- a/tdesign-component/lib/src/components/count_down/td_count_down.dart +++ b/tdesign-component/lib/src/components/time_counter/td_time_counter.dart @@ -4,8 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import '../../../tdesign_flutter.dart'; import '../../util/context_extension.dart'; +import '../../util/list_ext.dart'; -RegExp _timeReg = RegExp(r'D{2}|H{2}|m{2}|s{2}|S{3}'); +RegExp _timeReg = RegExp(r'D+|H+|m+|s+|S+'); String _toDigits(int n, int l) => n.toString().padLeft(l, '0'); @@ -17,21 +18,22 @@ String _getMark(String format, String? type) { return part.split('')[0]; } -/// 倒计时组件 -class TDCountDown extends StatefulWidget { - const TDCountDown({ +/// 计时组件 +class TDTimeCounter extends StatefulWidget { + const TDTimeCounter({ Key? key, this.autoStart = true, this.content = 'default', this.format = 'HH:mm:ss', this.millisecond = false, - this.size = TDCountDownSize.medium, + this.size = TDTimeCounterSize.medium, this.splitWithUnit = false, - this.theme = TDCountDownTheme.defaultTheme, + this.theme = TDTimeCounterTheme.defaultTheme, required this.time, this.style, this.onChange, this.onFinish, + this.direction = TDTimeCounterDirection.down, this.controller, }) : super(key: key); @@ -41,54 +43,55 @@ class TDCountDown extends StatefulWidget { /// 'default' / Widget Function(int time) / Widget final dynamic content; - /// 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒 + /// 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒(分隔符必须为长度为1的非空格的字符) final String format; /// 是否开启毫秒级渲染 final bool millisecond; - /// 倒计时尺寸 - final TDCountDownSize size; + /// 尺寸 + final TDTimeCounterSize size; /// 使用时间单位分割 final bool splitWithUnit; - /// 倒计时风格 - final TDCountDownTheme theme; + /// 风格 + final TDTimeCounterTheme theme; - /// 必需;倒计时时长,单位毫秒 + /// 必需;计时时长,单位毫秒 final int time; /// 自定义样式,有则优先用它,没有则根据size和theme选取 - final TDCountDownStyle? style; + final TDTimeCounterStyle? style; /// 时间变化时触发回调 final Function(int time)? onChange; - /// 倒计时结束时触发回调 + /// 计时结束时触发回调 final VoidCallback? onFinish; + /// 计时方向,默认倒计时 + final TDTimeCounterDirection direction; + /// 控制器,可控制开始/暂停/继续/重置 - final TDCountDownController? controller; + final TDTimeCounterController? controller; @override - _TDCountDownState createState() => _TDCountDownState(); + _TDTimeCounterState createState() => _TDTimeCounterState(); } -class _TDCountDownState extends State with SingleTickerProviderStateMixin { - late TDCountDownStyle _style; +class _TDTimeCounterState extends State with SingleTickerProviderStateMixin { + late TDTimeCounterStyle _style; late Map timeUnitMap; Ticker? _ticker; int _time = 0; int _tempMilliseconds = 0; + int _maxTime = 0; @override void initState() { super.initState(); - _time = widget.time; - if (widget.autoStart) { - startTimer(); - } + resetTimer(widget.time, false); widget.controller?.addListener(_onControllerChanged); } @@ -96,28 +99,31 @@ class _TDCountDownState extends State with SingleTickerProviderStat void didChangeDependencies() { super.didChangeDependencies(); _style = widget.style ?? - TDCountDownStyle.generateStyle( + TDTimeCounterStyle.generateStyle( context, size: widget.size, theme: widget.theme, splitWithUnit: widget.splitWithUnit, ); timeUnitMap = { - 'DD': context.resource.days, - 'HH': context.resource.hours, - 'mm': context.resource.minutes, - 'ss': context.resource.seconds, - 'SSS': context.resource.milliseconds, + 'D': context.resource.days, + 'H': context.resource.hours, + 'm': context.resource.minutes, + 's': context.resource.seconds, + 'S': context.resource.milliseconds, }; } @override - void didUpdateWidget(TDCountDown oldWidget) { + void didUpdateWidget(TDTimeCounter oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { oldWidget.controller?.removeListener(_onControllerChanged); widget.controller?.addListener(_onControllerChanged); } + if (widget.time != oldWidget.time) { + resetTimer(widget.time, false); + } } @override @@ -134,13 +140,19 @@ class _TDCountDownState extends State with SingleTickerProviderStat } _tempMilliseconds = 0; _ticker ??= createTicker((Duration elapsed) { - if (_time > 0) { - _time = max(_time - (elapsed.inMilliseconds - _tempMilliseconds), 0); + if ((widget.direction == TDTimeCounterDirection.down && _time > 0) || + widget.direction == TDTimeCounterDirection.up && _time < _maxTime) { + setState(() { + if (widget.direction == TDTimeCounterDirection.down) { + _time = max(_time - (elapsed.inMilliseconds - _tempMilliseconds), 0); + } else { + _time = min(_time + (elapsed.inMilliseconds - _tempMilliseconds), _maxTime); + } + }); _tempMilliseconds = elapsed.inMilliseconds; widget.onChange?.call(_time); } else { - _time = 0; - _ticker!.stop(); + pauseTimer(); widget.onFinish?.call(); } setState(() {}); @@ -158,28 +170,37 @@ class _TDCountDownState extends State with SingleTickerProviderStat startTimer(); } - /// 重置倒计时 - void resetTimer([int? time]) { + /// 重置计时 + void resetTimer([int? time, bool update = true]) { _ticker?.stop(); - _time = time ?? widget.time; - setState(() {}); + if (widget.direction == TDTimeCounterDirection.down) { + _time = time ?? widget.time; + } else { + _time = 0; + _maxTime = time ?? widget.time; + } + if (update) { + setState(() {}); + } if (widget.autoStart) { - startTimer(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + startTimer(); + }); } } void _onControllerChanged() { switch (widget.controller?.value) { - case TDCountDownStatus.start: + case TDTimeCounterStatus.start: startTimer(); break; - case TDCountDownStatus.pause: + case TDTimeCounterStatus.pause: pauseTimer(); break; - case TDCountDownStatus.resume: + case TDTimeCounterStatus.resume: resumeTimer(); break; - case TDCountDownStatus.reset: + case TDTimeCounterStatus.reset: resetTimer(widget.controller?.time); break; default: @@ -199,16 +220,15 @@ class _TDCountDownState extends State with SingleTickerProviderStat } List _buildTimeWidget(BuildContext context) { - var format = widget.millisecond ? '${widget.format.replaceAll(':SSS', '')}:SSS' : widget.format; - var matches = _timeReg.allMatches(format); - var timeMap = _getTimeMap(_time); + final format = widget.millisecond ? '${widget.format.replaceAll(RegExp(r':S+$'), '')}:SSS' : widget.format; + final matches = _timeReg.allMatches(format); + final timeMap = _getTimeMap(matches.map((e) => e.group(0) ?? '').toList()); return matches .map((match) { - var timeType = match.group(0); - var timeData = timeUnitMap[timeType] ?? ''; + final timeType = match.group(0) ?? ''; return _buildTextWidget( timeMap[timeType] ?? '0', - widget.splitWithUnit ? timeData : _getMark(format, timeType), + widget.splitWithUnit ? timeUnitMap[timeType[0]] ?? '' : _getMark(format, timeType), ); }) .expand((element) => element) @@ -219,7 +239,7 @@ class _TDCountDownState extends State with SingleTickerProviderStat String time, String split, ) { - var children = [ + final children = [ Container( width: _style.timeWidth, height: _style.timeHeight, @@ -259,13 +279,44 @@ class _TDCountDownState extends State with SingleTickerProviderStat return children; } - Map _getTimeMap(int m) { - var duration = Duration(milliseconds: m); - var days = _toDigits(duration.inDays, 2); - var hours = _toDigits(duration.inHours, 2); - var minutes = _toDigits(duration.inMinutes.remainder(60), 2); - var seconds = _toDigits(duration.inSeconds.remainder(60), 2); - var milliseconds = _toDigits(duration.inMilliseconds.remainder(1000), 3); - return {'DD': days, 'HH': hours, 'mm': minutes, 'ss': seconds, 'SSS': milliseconds}; + Map _getTimeMap(List timeType) { + var duration = Duration(milliseconds: _time); + final map = {}; + final dayKey = timeType.find((item) => item.startsWith('D')); + final hourKey = timeType.find((item) => item.startsWith('H')); + final minuteKey = timeType.find((item) => item.startsWith('m')); + final secondKey = timeType.find((item) => item.startsWith('s')); + final millisecondKey = timeType.find((item) => item.startsWith('S')); + if (dayKey != null) { + final length = dayKey.length; + map[dayKey] = _toDigits(duration.inDays, length); + duration = duration - Duration(days: duration.inDays); + } + if (hourKey != null) { + final length = hourKey.length; + final upNum = length > 2 ? pow(10, length).toInt() : 24; + final time = duration.inHours.remainder(upNum); + map[hourKey] = _toDigits(time, length); + duration = duration - Duration(hours: time); + } + if (minuteKey != null) { + final length = minuteKey.length; + final upNum = length > 2 ? pow(10, length).toInt() : 60; + final time = duration.inMinutes.remainder(upNum); + map[minuteKey] = _toDigits(time, length); + duration = duration - Duration(minutes: time); + } + if (secondKey != null) { + final length = secondKey.length; + final upNum = length > 2 ? pow(10, length).toInt() : 60; + final time = duration.inSeconds.remainder(upNum); + map[secondKey] = _toDigits(time, length); + duration = duration - Duration(seconds: time); + } + if (millisecondKey != null) { + final length = millisecondKey.length; + map[millisecondKey] = _toDigits(duration.inMilliseconds, length); + } + return map; } } diff --git a/tdesign-component/lib/src/components/count_down/td_count_down_controller.dart b/tdesign-component/lib/src/components/time_counter/td_time_counter_controller.dart similarity index 58% rename from tdesign-component/lib/src/components/count_down/td_count_down_controller.dart rename to tdesign-component/lib/src/components/time_counter/td_time_counter_controller.dart index dc4c3d2a8..d1744ed46 100644 --- a/tdesign-component/lib/src/components/count_down/td_count_down_controller.dart +++ b/tdesign-component/lib/src/components/time_counter/td_time_counter_controller.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; -/// 倒计时组件控制器转态 -enum TDCountDownStatus { +/// 计时组件控制器转态 +enum TDTimeCounterStatus { /// 开始 start, @@ -19,8 +19,8 @@ enum TDCountDownStatus { } /// 倒计时组件控制器,可控制开始(`start()`)/暂停(`pause()`)/继续(`resume()`)/重置(`reset([int? time])`) -class TDCountDownController extends ValueNotifier { - TDCountDownController() : super(TDCountDownStatus.idle); +class TDTimeCounterController extends ValueNotifier { + TDTimeCounterController() : super(TDTimeCounterStatus.idle); int? _time; @@ -28,27 +28,27 @@ class TDCountDownController extends ValueNotifier { /// 开始 void start() { - value = TDCountDownStatus.start; + value = TDTimeCounterStatus.start; } /// 暂停 void pause() { - value = TDCountDownStatus.pause; + value = TDTimeCounterStatus.pause; } /// 继续 void resume() { - value = TDCountDownStatus.resume; + value = TDTimeCounterStatus.resume; } /// 重置 void reset([int? time]) { - if (value == TDCountDownStatus.reset && _time != time) { + if (value == TDTimeCounterStatus.reset) { _time = time; notifyListeners(); } else { _time = time; - value = TDCountDownStatus.reset; + value = TDTimeCounterStatus.reset; } } } diff --git a/tdesign-component/lib/src/components/count_down/td_count_down_style.dart b/tdesign-component/lib/src/components/time_counter/td_time_counter_style.dart similarity index 81% rename from tdesign-component/lib/src/components/count_down/td_count_down_style.dart rename to tdesign-component/lib/src/components/time_counter/td_time_counter_style.dart index 5f5e658c5..ceaedf08c 100644 --- a/tdesign-component/lib/src/components/count_down/td_count_down_style.dart +++ b/tdesign-component/lib/src/components/time_counter/td_time_counter_style.dart @@ -1,8 +1,16 @@ import 'package:flutter/material.dart'; import '../../../tdesign_flutter.dart'; -/// 倒计时组件尺寸 -enum TDCountDownSize { +/// 计时组件计时方向 +enum TDTimeCounterDirection { + /// 倒计时 + down, + /// 正向计时 + up +} + +/// 计时组件尺寸 +enum TDTimeCounterSize { /// 小 small, @@ -13,8 +21,8 @@ enum TDCountDownSize { large, } -/// 倒计时组件风格 -enum TDCountDownTheme { +/// 计时组件风格 +enum TDTimeCounterTheme { /// 默认 defaultTheme, @@ -25,9 +33,9 @@ enum TDCountDownTheme { square, } -/// 倒计时组件样式 -class TDCountDownStyle { - TDCountDownStyle({ +/// 计时组件样式 +class TDTimeCounterStyle { + TDTimeCounterStyle({ this.timeWidth, this.timeHeight, this.timePadding, @@ -91,17 +99,17 @@ class TDCountDownStyle { double? space; /// 生成默认样式 - TDCountDownStyle.generateStyle( + TDTimeCounterStyle.generateStyle( BuildContext context, { - TDCountDownSize? size, - TDCountDownTheme? theme, + TDTimeCounterSize? size, + TDTimeCounterTheme? theme, bool? splitWithUnit, }) { timeFontFamily = TDTheme.defaultData().numberFontFamily; late Font? font; - switch (size ?? TDCountDownSize.medium) { - case TDCountDownSize.small: - if (theme == TDCountDownTheme.defaultTheme) { + switch (size ?? TDTimeCounterSize.medium) { + case TDTimeCounterSize.small: + if (theme == TDTimeCounterTheme.defaultTheme) { timeWidth = timeHeight = null; font = TDTheme.of(context).fontBodyMedium; timeFontSize = splitFontSize = font?.size ?? 14; @@ -114,8 +122,8 @@ class TDCountDownStyle { } space = TDTheme.of(context).spacer4 / 2; break; - case TDCountDownSize.medium: - if (theme == TDCountDownTheme.defaultTheme) { + case TDTimeCounterSize.medium: + if (theme == TDTimeCounterTheme.defaultTheme) { timeWidth = timeHeight = null; font = TDTheme.of(context).fontBodyLarge; timeFontSize = splitFontSize = font?.size ?? 16; @@ -128,8 +136,8 @@ class TDCountDownStyle { } space = TDTheme.of(context).spacer8 / 2; break; - case TDCountDownSize.large: - if (theme == TDCountDownTheme.defaultTheme) { + case TDTimeCounterSize.large: + if (theme == TDTimeCounterTheme.defaultTheme) { timeWidth = timeHeight = null; font = TDTheme.of(context).fontBodyExtraLarge; timeFontSize = splitFontSize = font?.size ?? 18; @@ -143,8 +151,8 @@ class TDCountDownStyle { space = TDTheme.of(context).spacer12 / 2; } - switch (theme ?? TDCountDownTheme.defaultTheme) { - case TDCountDownTheme.round: + switch (theme ?? TDTimeCounterTheme.defaultTheme) { + case TDTimeCounterTheme.round: timeBox = BoxDecoration( shape: BoxShape.circle, color: TDTheme.of(context).errorColor6, @@ -152,7 +160,7 @@ class TDCountDownStyle { timeColor = TDTheme.of(context).fontWhColor1; splitColor = TDTheme.of(context).errorColor6; break; - case TDCountDownTheme.square: + case TDTimeCounterTheme.square: timeBox = BoxDecoration( shape: BoxShape.rectangle, borderRadius: BorderRadius.circular(TDTheme.of(context).radiusSmall), @@ -161,7 +169,7 @@ class TDCountDownStyle { timeColor = TDTheme.of(context).fontWhColor1; splitColor = TDTheme.of(context).errorColor6; break; - case TDCountDownTheme.defaultTheme: + case TDTimeCounterTheme.defaultTheme: timeBox = null; timeColor = splitColor = TDTheme.of(context).fontGyColor1; timeWidth = null; diff --git a/tdesign-component/lib/src/components/toast/td_toast.dart b/tdesign-component/lib/src/components/toast/td_toast.dart index fa24540a9..c30ca481a 100644 --- a/tdesign-component/lib/src/components/toast/td_toast.dart +++ b/tdesign-component/lib/src/components/toast/td_toast.dart @@ -15,8 +15,9 @@ class TDToast { {required BuildContext context, Duration duration = TDToast._defaultDisPlayDuration, int? maxLines, - BoxConstraints? constraints}) { - _showOverlay(_TDTextToast(text: text, maxLines: maxLines, constraints: constraints), context: context, duration: duration); + BoxConstraints? constraints, + bool? preventTap}) { + _showOverlay(_TDTextToast(text: text, maxLines: maxLines, constraints: constraints), context: context, duration: duration, preventTap: preventTap); } /// 带图标的Toast @@ -24,7 +25,8 @@ class TDToast { {IconData? icon, IconTextDirection direction = IconTextDirection.horizontal, required BuildContext context, - Duration duration = TDToast._defaultDisPlayDuration}) { + Duration duration = TDToast._defaultDisPlayDuration, + bool? preventTap}) { _showOverlay( _TDIconTextToast( text: text, @@ -32,14 +34,16 @@ class TDToast { iconTextDirection: direction, ), context: context, - duration: duration); + duration: duration, + preventTap: preventTap); } /// 成功提示Toast static void showSuccess(String? text, {IconTextDirection direction = IconTextDirection.horizontal, required BuildContext context, - Duration duration = TDToast._defaultDisPlayDuration}) { + Duration duration = TDToast._defaultDisPlayDuration, + bool? preventTap}) { _showOverlay( _TDIconTextToast( text: text, @@ -47,14 +51,16 @@ class TDToast { iconTextDirection: direction, ), context: context, - duration: duration); + duration: duration, + preventTap: preventTap); } /// 警告Toast static void showWarning(String? text, {IconTextDirection direction = IconTextDirection.horizontal, required BuildContext context, - Duration duration = TDToast._defaultDisPlayDuration}) { + Duration duration = TDToast._defaultDisPlayDuration, + bool? preventTap}) { _showOverlay( _TDIconTextToast( text: text, @@ -62,14 +68,16 @@ class TDToast { iconTextDirection: direction, ), context: context, - duration: duration); + duration: duration, + preventTap: preventTap); } /// 失败提示Toast static void showFail(String? text, {IconTextDirection direction = IconTextDirection.horizontal, required BuildContext context, - Duration duration = TDToast._defaultDisPlayDuration}) { + Duration duration = TDToast._defaultDisPlayDuration, + bool? preventTap}) { _showOverlay( _TDIconTextToast( text: text, @@ -77,24 +85,26 @@ class TDToast { iconTextDirection: direction, ), context: context, - duration: duration); + duration: duration, + preventTap: preventTap); } /// 带文案的加载Toast static void showLoading( - {required BuildContext context, String? text, Duration duration = TDToast._infiniteDuration}) { + {required BuildContext context, String? text, Duration duration = TDToast._infiniteDuration, bool? preventTap}) { _showOverlay( _TDToastLoading( text: text, ), context: context, - duration: duration); + duration: duration, + preventTap: preventTap); } /// 不带文案的加载Toast static void showLoadingWithoutText( - {required BuildContext context, String? text, Duration duration = TDToast._infiniteDuration}) { - _showOverlay(const _TDToastLoadingWithoutText(), context: context, duration: duration); + {required BuildContext context, String? text, Duration duration = TDToast._infiniteDuration, bool? preventTap}) { + _showOverlay(const _TDToastLoadingWithoutText(), context: context, duration: duration, preventTap: preventTap); } /// 关闭加载Toast @@ -103,18 +113,41 @@ class TDToast { } static void _showOverlay(Widget? widget, - {required BuildContext context, Duration duration = TDToast._defaultDisPlayDuration}) { + {required BuildContext context, Duration duration = TDToast._defaultDisPlayDuration, + bool? preventTap}) { _cancel(); _showing = true; var overlayState = Overlay.of(context); _overlayEntry = OverlayEntry( builder: (BuildContext context) => Center( - child: AnimatedOpacity( - opacity: _showing ? 1.0 : 0.0, - duration: _showing ? const Duration(milliseconds: 100) : const Duration(milliseconds: 200), - child: widget, + child: AnimatedOpacity( + opacity: _showing ? 1.0 : 0.0, + duration: _showing ? const Duration(milliseconds: 100) : const Duration(milliseconds: 200), + child: widget, + ), + )); + + if(preventTap ?? false) { + _overlayEntry = OverlayEntry( + builder: (BuildContext context) => Positioned( + top: 0, + right: 0, + bottom: 0, + left: 0, + child: Container( + color: Colors.transparent, + child: Align( + alignment: Alignment.center, + child: AnimatedOpacity( + opacity: _showing ? 1.0 : 0.0, + duration: _showing ? const Duration(milliseconds: 100) : const Duration(milliseconds: 200), + child: widget, + ), ), - )); + ), + ), + ); + } if (_overlayEntry != null) { overlayState?.insert(_overlayEntry!); } diff --git a/tdesign-component/lib/src/theme/resource_delegate.dart b/tdesign-component/lib/src/theme/resource_delegate.dart index caa6bcb08..093e20018 100644 --- a/tdesign-component/lib/src/theme/resource_delegate.dart +++ b/tdesign-component/lib/src/theme/resource_delegate.dart @@ -6,23 +6,23 @@ typedef TDTDResourceBuilder = TDResourceDelegate? Function(BuildContext context) /// 资源管理器 class TDResourceManager { - /// 代理构建器 TDTDResourceBuilder? _builder; + /// 每次都调用build方法 bool _needAlwaysBuild = false; TDResourceDelegate? _delegate; /// 获取资源 - TDResourceDelegate delegate(BuildContext context){ - if(_builder == null){ + TDResourceDelegate delegate(BuildContext context) { + if (_builder == null) { return _defaultDelegate; } - if(_needAlwaysBuild){ + if (_needAlwaysBuild) { // 每次都调用,适用于全局有多个TDResourceDelegate的情况 var delegate = _builder?.call(context); - if(delegate != null){ + if (delegate != null) { return delegate; } } @@ -42,7 +42,7 @@ class TDResourceManager { static final _defaultDelegate = _DefaultResourceDelegate(); /// 设置资源代理 - void setResourceBuilder(TDTDResourceBuilder delegate,needAlwaysBuild) { + void setResourceBuilder(TDTDResourceBuilder delegate, needAlwaysBuild) { _builder = delegate; _needAlwaysBuild = needAlwaysBuild; } @@ -86,20 +86,89 @@ abstract class TDResourceDelegate { /// [TDRefreshHeader] 松开刷新 String get releaseRefresh; - /// [TDCountDown] 天 + /// [TDTimeCounter] 天 String get days; - /// [TDCountDown] 时 + /// [TDTimeCounter] 时 String get hours; - /// [TDCountDown] 分 + /// [TDTimeCounter] 分 String get minutes; - /// [TDCountDown] 秒 + /// [TDTimeCounter] 秒 String get seconds; - /// [TDCountDown] 毫秒 + /// [TDTimeCounter] 毫秒 String get milliseconds; + + /// [TDCalendarHeader] 星期日 + String get sunday; + + /// [TDCalendarHeader] 星期一 + String get monday; + + /// [TDCalendarHeader] 星期二 + String get tuesday; + + /// [TDCalendarHeader] 星期三 + String get wednesday; + + /// [TDCalendarHeader] 星期四 + String get thursday; + + /// [TDCalendarHeader] 星期五 + String get friday; + + /// [TDCalendarHeader] 星期六 + String get saturday; + + /// [TDCalendarBody] 年 + String get year; + + /// [TDCalendarBody] 一月 + String get january; + + /// [TDCalendarBody] 二月 + String get february; + + /// [TDCalendarBody] 三月 + String get march; + + /// [TDCalendarBody] 四月 + String get april; + + /// [TDCalendarBody] 五月 + String get may; + + /// [TDCalendarBody] 六月 + String get june; + + /// [TDCalendarBody] 七月 + String get july; + + /// [TDCalendarBody] 八月 + String get august; + + /// [TDCalendarBody] 九月 + String get september; + + /// [TDCalendarBody] 十月 + String get october; + + /// [TDCalendarBody] 十一月 + String get november; + + /// [TDCalendarBody] 十二月 + String get december; + + /// [TDCalendar] 时间 + String get time; + + /// [TDCalendar] 开始 + String get start; + + /// [TDCalendar] 结束 + String get end; } /// 如果用户要重写,就应该全部重写,不开放只重新部分资源 @@ -139,7 +208,7 @@ class _DefaultResourceDelegate extends TDResourceDelegate { @override String get releaseRefresh => '松开刷新'; - + @override String get days => '天'; @@ -154,4 +223,73 @@ class _DefaultResourceDelegate extends TDResourceDelegate { @override String get milliseconds => '毫秒'; + + @override + String get sunday => '日'; + + @override + String get monday => '一'; + + @override + String get tuesday => '二'; + + @override + String get wednesday => '三'; + + @override + String get thursday => '四'; + + @override + String get friday => '五'; + + @override + String get saturday => '六'; + + @override + String get year => ' 年'; + + @override + String get january => '1 月'; + + @override + String get february => '2 月'; + + @override + String get march => '3 月'; + + @override + String get april => '4 月'; + + @override + String get may => '5 月'; + + @override + String get june => '6 月'; + + @override + String get july => '7 月'; + + @override + String get august => '8 月'; + + @override + String get september => '9 月'; + + @override + String get october => '10 月'; + + @override + String get november => '11 月'; + + @override + String get december => '12 月'; + + @override + String get time => '时间'; + + @override + String get start => '开始'; + + @override + String get end => '结束'; } diff --git a/tdesign-component/lib/src/util/iterable_ext.dart b/tdesign-component/lib/src/util/iterable_ext.dart index 97c0ce3c8..fca681e62 100644 --- a/tdesign-component/lib/src/util/iterable_ext.dart +++ b/tdesign-component/lib/src/util/iterable_ext.dart @@ -82,5 +82,15 @@ extension IterableExt on Iterable { return firstWhereOrNull((element) => test(element)) != null; } + /// + /// 获取指定索引的元素,如果索引越界,则返回null + /// + T? getOrNull(int index) { + if (index < 0 || index >= length) { + return null; + } + return elementAt(index); + } + } \ No newline at end of file diff --git a/tdesign-component/lib/tdesign_flutter.dart b/tdesign-component/lib/tdesign_flutter.dart index c995c207f..d37ae4a7b 100644 --- a/tdesign-component/lib/tdesign_flutter.dart +++ b/tdesign-component/lib/tdesign_flutter.dart @@ -3,6 +3,7 @@ export 'src/components/backtop/td_backtop.dart'; export 'src/components/badge/td_badge.dart'; export 'src/components/button/td_button.dart'; export 'src/components/button/td_button_style.dart'; +export 'src/components/calendar/td_calendar.dart'; export 'src/components/cascader/td_cascader.dart'; export 'src/components/cascader/td_custom_tab.dart'; export 'src/components/cascader/td_multi_cascader.dart'; @@ -13,9 +14,6 @@ export 'src/components/checkbox/td_check_box.dart'; export 'src/components/checkbox/td_check_box_group.dart'; export 'src/components/collapse/td_collapse.dart'; export 'src/components/collapse/td_collapse_panel.dart'; -export 'src/components/count_down/td_count_down.dart'; -export 'src/components/count_down/td_count_down_controller.dart'; -export 'src/components/count_down/td_count_down_style.dart'; export 'src/components/dialog/td_dialog.dart'; export 'src/components/divider/td_divider.dart'; export 'src/components/drawer/td_drawer.dart'; @@ -28,6 +26,7 @@ export 'src/components/image/image_widget.dart'; export 'src/components/image/td_image.dart'; export 'src/components/image_viewer/td_image_viewer.dart'; export 'src/components/image_viewer/td_image_viewer_widget.dart'; +export 'src/components/indexes/td_indexes.dart'; export 'src/components/input/input_view.dart'; export 'src/components/input/td_input.dart'; export 'src/components/input/td_input_spacer.dart'; @@ -36,6 +35,8 @@ export 'src/components/loading/td_circle_indicator.dart'; export 'src/components/loading/td_loading.dart'; export 'src/components/loading/td_loading_controller.dart'; export 'src/components/navbar/td_nav_bar.dart'; +export 'src/components/notice_bar/td_notice_bar.dart'; +export 'src/components/notice_bar/td_notice_bar_style.dart'; export 'src/components/picker/td_date_picker.dart'; export 'src/components/picker/td_item_widget.dart'; export 'src/components/picker/td_multi_picker.dart'; @@ -70,6 +71,9 @@ export 'src/components/tag/td_tag_styles.dart'; export 'src/components/text/td_font_loader.dart'; export 'src/components/text/td_text.dart'; export 'src/components/textarea/td_textarea.dart'; +export 'src/components/time_counter/td_time_counter.dart'; +export 'src/components/time_counter/td_time_counter_controller.dart'; +export 'src/components/time_counter/td_time_counter_style.dart'; export 'src/components/toast/td_toast.dart'; export 'src/components/tree/td_tree_select.dart'; export 'src/theme/basic.dart'; diff --git a/tdesign-site/site/docs/getting-started.md b/tdesign-site/site/docs/getting-started.md index 938d85f05..654fa5cb1 100644 --- a/tdesign-site/site/docs/getting-started.md +++ b/tdesign-site/site/docs/getting-started.md @@ -157,7 +157,7 @@ class IntlResourceDelegate extends TDResourceDelegate { ## 组件规划 -- 开发中组件: NoticeBar,Rate,Calendar,Indexs,ActionSheet,Progress,Footer,Result,Message,Popover,Table +- 开发中组件: NoticeBar,Rate,Calendar,Indexes,ActionSheet,Progress,Footer,Result,Message,Popover,Table - 待开发组件: Form,Upload diff --git a/tdesign-site/site/docs/overview.md b/tdesign-site/site/docs/overview.md index 7207d6fcd..a0ca0a670 100644 --- a/tdesign-site/site/docs/overview.md +++ b/tdesign-site/site/docs/overview.md @@ -270,10 +270,10 @@ spline: explain diff --git a/tdesign-site/site/site.config.mjs b/tdesign-site/site/site.config.mjs index 87f50b344..715e2abee 100644 --- a/tdesign-site/site/site.config.mjs +++ b/tdesign-site/site/site.config.mjs @@ -309,11 +309,11 @@ export default { component: () => import('@/collapse/README.md'), }, { - title: 'CountDown 倒计时', - name: 'countdown', + title: 'TimeCounter 计时器', + name: 'timeCounter', meta: { docType: 'data' }, - path: '/flutter/components/count-down', - component: () => import('@/count-down/README.md'), + path: '/flutter/components/time-counter', + component: () => import('@/time-counter/README.md'), }, { title: 'Empty 空状态', diff --git a/tdesign-site/src/count-down/README.md b/tdesign-site/src/count-down/README.md deleted file mode 100644 index 011807705..000000000 --- a/tdesign-site/src/count-down/README.md +++ /dev/null @@ -1,637 +0,0 @@ ---- -title: CountDown 倒计时 -description: 用于实时展示倒计时数值。 -spline: base -isComponent: true ---- - - -## 引入 - -在tdesign_flutter/tdesign_flutter.dart中有所有组件的路径。 - -```dart -import 'package:tdesign_flutter/tdesign_flutter.dart'; -``` - -## 代码演示 - -[td_count_down_page.dart](https://github.com/Tencent/tdesign-flutter/blob/main/tdesign-component/example/lib/page/td_count_down_page.dart) - -### 1 组件类型 - -时分秒 - - - - -
-TDCountDown _buildSimple(BuildContext context) {
-  return const TDCountDown(time: 60 * 60 * 1000);
-}
- -
- - -带毫秒 - - - - -
-TDCountDown _buildMillisecondSimple(BuildContext context) {
-  return const TDCountDown(time: 60 * 60 * 1000, millisecond: true);
-}
- -
- - -带方形底 - - - - -
-TDCountDown _buildSquareSimple(BuildContext context) {
-  return const TDCountDown(time: 60 * 60 * 1000, theme: TDCountDownTheme.square);
-}
- -
- - -带圆形底 - - - - -
-TDCountDown _buildRoundSimple(BuildContext context) {
-  return const TDCountDown(time: 60 * 60 * 1000, theme: TDCountDownTheme.round);
-}
- -
- - -带单位 - - - - -
-TDCountDown _buildUnitSimple(BuildContext context) {
-  return const TDCountDown(time: 60 * 60 * 1000, theme: TDCountDownTheme.square, splitWithUnit: true);
-}
- -
- - -无底色带单位 - - - - -
-TDCountDown _buildCustomUnitSimple(BuildContext context) {
-  var style = TDCountDownStyle.generateStyle(context);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(time: 60 * 60 * 1000, splitWithUnit: true, style: style);
-}
- -
- -### 1 组件尺寸 - -纯数字 - - - - -
-TDCountDown _buildSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-  );
-}
- -
- - - - - -
-TDCountDown _buildMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-  );
-}
- -
- - - - - -
-TDCountDown _buildLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-  );
-}
- -
- - - - - -
-TDCountDown _buildSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-  );
-}
- -
- - - - - -
-TDCountDown _buildMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-  );
-}
- -
- - - - - -
-TDCountDown _buildLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-  );
-}
- -
- - -带方形底 - - - - -
-TDCountDown _buildSquareSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-    theme: TDCountDownTheme.square,
-  );
-}
- -
- - - - - -
-TDCountDown _buildSquareMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-    theme: TDCountDownTheme.square,
-  );
-}
- -
- - - - - -
-TDCountDown _buildSquareLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-    theme: TDCountDownTheme.square,
-  );
-}
- -
- - - - - -
-TDCountDown _buildSquareSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-    theme: TDCountDownTheme.square,
-  );
-}
- -
- - - - - -
-TDCountDown _buildSquareMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-    theme: TDCountDownTheme.square,
-  );
-}
- -
- - - - - -
-TDCountDown _buildSquareLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-    theme: TDCountDownTheme.square,
-  );
-}
- -
- - -带圆形底 - - - - -
-TDCountDown _buildRoundSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-    theme: TDCountDownTheme.round,
-  );
-}
- -
- - - - - -
-TDCountDown _buildRoundMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-    theme: TDCountDownTheme.round,
-  );
-}
- -
- - - - - -
-TDCountDown _buildRoundLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-    theme: TDCountDownTheme.round,
-  );
-}
- -
- - - - - -
-TDCountDown _buildRoundSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-    theme: TDCountDownTheme.round,
-  );
-}
- -
- - - - - -
-TDCountDown _buildRoundMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-    theme: TDCountDownTheme.round,
-  );
-}
- -
- - - - - -
-TDCountDown _buildRoundLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-    theme: TDCountDownTheme.round,
-  );
-}
- -
- - -带单位 - - - - -
-TDCountDown _buildUnitSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-    theme: TDCountDownTheme.square,
-    splitWithUnit: true,
-  );
-}
- -
- - - - - -
-TDCountDown _buildUnitMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-    theme: TDCountDownTheme.square,
-    splitWithUnit: true,
-  );
-}
- -
- - - - - -
-TDCountDown _buildUnitLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-    theme: TDCountDownTheme.square,
-    splitWithUnit: true,
-  );
-}
- -
- - - - - -
-TDCountDown _buildUnitSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-    theme: TDCountDownTheme.square,
-    splitWithUnit: true,
-  );
-}
- -
- - - - - -
-TDCountDown _buildUnitMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-    theme: TDCountDownTheme.square,
-    splitWithUnit: true,
-  );
-}
- -
- - - - - -
-TDCountDown _buildUnitLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-    theme: TDCountDownTheme.square,
-    splitWithUnit: true,
-  );
-}
- -
- - -无底色带单位 - - - - -
-TDCountDown _buildCustomUnitSmallSize(BuildContext context) {
-  var style = TDCountDownStyle.generateStyle(context, size: TDCountDownSize.small);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(
-    time: 60 * 60 * 1000,
-    splitWithUnit: true,
-    style: style,
-  );
-}
- -
- - - - - -
-TDCountDown _buildCustomUnitMediumSize(BuildContext context) {
-  var style = TDCountDownStyle.generateStyle(context, size: TDCountDownSize.medium);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(
-    time: 60 * 60 * 1000,
-    splitWithUnit: true,
-    style: style,
-  );
-}
- -
- - - - - -
-TDCountDown _buildCustomUnitLargeSize(BuildContext context) {
-  var style = TDCountDownStyle.generateStyle(context, size: TDCountDownSize.large);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(
-    time: 60 * 60 * 1000,
-    splitWithUnit: true,
-    style: style,
-  );
-}
- -
- - - - - -
-TDCountDown _buildCustomUnitSmallSize(BuildContext context) {
-  var style = TDCountDownStyle.generateStyle(context, size: TDCountDownSize.small);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(
-    time: 60 * 60 * 1000,
-    splitWithUnit: true,
-    style: style,
-  );
-}
- -
- - - - - -
-TDCountDown _buildCustomUnitMediumSize(BuildContext context) {
-  var style = TDCountDownStyle.generateStyle(context, size: TDCountDownSize.medium);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(
-    time: 60 * 60 * 1000,
-    splitWithUnit: true,
-    style: style,
-  );
-}
- -
- - - - - -
-TDCountDown _buildCustomUnitLargeSize(BuildContext context) {
-  var style = TDCountDownStyle.generateStyle(context, size: TDCountDownSize.large);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(
-    time: 60 * 60 * 1000,
-    splitWithUnit: true,
-    style: style,
-  );
-}
- -
- - - -## API -### TDCountDown -#### 简介 -倒计时组件 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| key | | - | | -| autoStart | bool | true | 是否自动开始倒计时 | -| content | dynamic | 'default' | 'default' / Widget Function(int time) / Widget | -| format | String | 'HH:mm:ss' | 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒 | -| millisecond | bool | false | 是否开启毫秒级渲染 | -| size | TDCountDownSize | TDCountDownSize.medium | 倒计时尺寸 | -| splitWithUnit | bool | false | 使用时间单位分割 | -| theme | TDCountDownTheme | TDCountDownTheme.defaultTheme | 倒计时风格 | -| time | int | - | 必需;倒计时时长,单位毫秒 | -| style | TDCountDownStyle? | - | 自定义样式,有则优先用它,没有则根据size和theme选取 | -| onChange | Function(int time)? | - | 时间变化时触发回调 | -| onFinish | VoidCallback? | - | 倒计时结束时触发回调 | -| controller | TDCountDownController? | - | 控制器,可控制开始/暂停/继续/重置 | - -``` -``` - ### TDCountDownStyle -#### 简介 -倒计时组件样式 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| timeWidth | double? | - | 时间容器宽度 | -| timeHeight | double? | - | 时间容器高度 | -| timePadding | EdgeInsets? | - | 时间容器内边距 | -| timeMargin | EdgeInsets? | - | 时间容器外边距 | -| timeBox | BoxDecoration? | - | 时间容器装饰 | -| timeFontFamily | FontFamily? | - | 时间字体 | -| timeFontSize | double? | - | 时间字体尺寸 | -| timeFontHeight | double? | - | 时间字体行高 | -| timeFontWeight | FontWeight? | - | 时间字体粗细 | -| timeColor | Color? | - | 时间字体颜色 | -| splitFontSize | double? | - | 分隔符字体尺寸 | -| splitFontHeight | double? | - | 分隔符字体行高 | -| splitFontWeight | FontWeight? | - | 分隔符字体粗细 | -| splitColor | Color? | - | 分隔符字体颜色 | -| space | double? | - | 时间与分隔符的间隔 | - - -#### 工厂构造方法 - -| 名称 | 说明 | -| --- | --- | -| TDCountDownStyle.generateStyle | 生成默认样式 | - -``` -``` - ### TDCountDownController -#### 简介 -倒计时组件控制器,可控制开始(`start()`)/暂停(`pause()`)/继续(`resume()`)/重置(`reset([int? time])`) - - \ No newline at end of file diff --git a/tdesign-site/src/count_down/README.md b/tdesign-site/src/count_down/README.md deleted file mode 100644 index 2ae6434ab..000000000 --- a/tdesign-site/src/count_down/README.md +++ /dev/null @@ -1,590 +0,0 @@ ---- -title: CountDown 倒计时 -description: 用于实时展示倒计时数值。 -spline: base -isComponent: true ---- - - -## 引入 - -在tdesign_flutter/tdesign_flutter.dart中有所有组件的路径。 - -```dart -import 'package:tdesign_flutter/tdesign_flutter.dart'; -``` - -## 代码演示 - -### 1 组件类型 - -时分秒 - - - - -
-TDCountDown _buildSimple(BuildContext context) {
-  return const TDCountDown(time: 60 * 60 * 1000);
-}
- -
- - -带毫秒 - - - - -
-TDCountDown _buildMillisecondSimple(BuildContext context) {
-  return const TDCountDown(time: 60 * 60 * 1000, millisecond: true);
-}
- -
- - -带方形底 - - - - -
-TDCountDown _buildSquareSimple(BuildContext context) {
-  return const TDCountDown(
-      time: 60 * 60 * 1000, theme: TDCountDownTheme.square);
-}
- -
- - -带圆形底 - - - - -
-TDCountDown _buildRoundSimple(BuildContext context) {
-  return const TDCountDown(time: 60 * 60 * 1000, theme: TDCountDownTheme.round);
-}
- -
- - -带单位 - - - - -
-TDCountDown _buildUnitSimple(BuildContext context) {
-  return const TDCountDown(
-      time: 60 * 60 * 1000,
-      theme: TDCountDownTheme.square,
-      splitWithUnit: true);
-}
- -
- - -无底色带单位 - - - - -
-TDCountDown _buildCustomUnitSimple(BuildContext context) {
-  var style = TDCountDownStyle.generateStyle(context);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(time: 60 * 60 * 1000, splitWithUnit: true, style: style);
-}
- -
- -### 1 组件尺寸 - -纯数字 - - - - -
-TDCountDown _buildSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-  );
-}
- -
- - - - - -
-TDCountDown _buildMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-  );
-}
- -
- - - - - -
-TDCountDown _buildLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-  );
-}
- -
- - - - - -
-TDCountDown _buildSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-  );
-}
- -
- - - - - -
-TDCountDown _buildMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-  );
-}
- -
- - - - - -
-TDCountDown _buildLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-  );
-}
- -
- - -带方形底 - - - - -
-TDCountDown _buildSquareSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-    theme: TDCountDownTheme.square,
-  );
-}
- -
- - - - - -
-TDCountDown _buildSquareMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-    theme: TDCountDownTheme.square,
-  );
-}
- -
- - - - - -
-TDCountDown _buildSquareLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-    theme: TDCountDownTheme.square,
-  );
-}
- -
- - - - - -
-TDCountDown _buildSquareSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-    theme: TDCountDownTheme.square,
-  );
-}
- -
- - - - - -
-TDCountDown _buildSquareMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-    theme: TDCountDownTheme.square,
-  );
-}
- -
- - - - - -
-TDCountDown _buildSquareLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-    theme: TDCountDownTheme.square,
-  );
-}
- -
- - -带圆形底 - - - - -
-TDCountDown _buildRoundSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-    theme: TDCountDownTheme.round,
-  );
-}
- -
- - - - - -
-TDCountDown _buildRoundMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-    theme: TDCountDownTheme.round,
-  );
-}
- -
- - - - - -
-TDCountDown _buildRoundLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-    theme: TDCountDownTheme.round,
-  );
-}
- -
- - - - - -
-TDCountDown _buildRoundSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-    theme: TDCountDownTheme.round,
-  );
-}
- -
- - - - - -
-TDCountDown _buildRoundMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-    theme: TDCountDownTheme.round,
-  );
-}
- -
- - - - - -
-TDCountDown _buildRoundLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-    theme: TDCountDownTheme.round,
-  );
-}
- -
- - -带单位 - - - - -
-TDCountDown _buildUnitSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-    theme: TDCountDownTheme.square,
-    splitWithUnit: true,
-  );
-}
- -
- - - - - -
-TDCountDown _buildUnitMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-    theme: TDCountDownTheme.square,
-    splitWithUnit: true,
-  );
-}
- -
- - - - - -
-TDCountDown _buildUnitLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-    theme: TDCountDownTheme.square,
-    splitWithUnit: true,
-  );
-}
- -
- - - - - -
-TDCountDown _buildUnitSmallSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.small,
-    theme: TDCountDownTheme.square,
-    splitWithUnit: true,
-  );
-}
- -
- - - - - -
-TDCountDown _buildUnitMediumSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.medium,
-    theme: TDCountDownTheme.square,
-    splitWithUnit: true,
-  );
-}
- -
- - - - - -
-TDCountDown _buildUnitLargeSize(BuildContext context) {
-  return const TDCountDown(
-    time: 60 * 60 * 1000,
-    size: TDCountDownSize.large,
-    theme: TDCountDownTheme.square,
-    splitWithUnit: true,
-  );
-}
- -
- - -无底色带单位 - - - - -
-TDCountDown _buildCustomUnitSmallSize(BuildContext context) {
-  var style =
-      TDCountDownStyle.generateStyle(context, size: TDCountDownSize.small);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(
-    time: 60 * 60 * 1000,
-    splitWithUnit: true,
-    style: style,
-  );
-}
- -
- - - - - -
-TDCountDown _buildCustomUnitMediumSize(BuildContext context) {
-  var style =
-      TDCountDownStyle.generateStyle(context, size: TDCountDownSize.medium);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(
-    time: 60 * 60 * 1000,
-    splitWithUnit: true,
-    style: style,
-  );
-}
- -
- - - - - -
-TDCountDown _buildCustomUnitLargeSize(BuildContext context) {
-  var style =
-      TDCountDownStyle.generateStyle(context, size: TDCountDownSize.large);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(
-    time: 60 * 60 * 1000,
-    splitWithUnit: true,
-    style: style,
-  );
-}
- -
- - - - - -
-TDCountDown _buildCustomUnitSmallSize(BuildContext context) {
-  var style =
-      TDCountDownStyle.generateStyle(context, size: TDCountDownSize.small);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(
-    time: 60 * 60 * 1000,
-    splitWithUnit: true,
-    style: style,
-  );
-}
- -
- - - - - -
-TDCountDown _buildCustomUnitMediumSize(BuildContext context) {
-  var style =
-      TDCountDownStyle.generateStyle(context, size: TDCountDownSize.medium);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(
-    time: 60 * 60 * 1000,
-    splitWithUnit: true,
-    style: style,
-  );
-}
- -
- - - - - -
-TDCountDown _buildCustomUnitLargeSize(BuildContext context) {
-  var style =
-      TDCountDownStyle.generateStyle(context, size: TDCountDownSize.large);
-  style.timeColor = TDTheme.of(context).errorColor6;
-  return TDCountDown(
-    time: 60 * 60 * 1000,
-    splitWithUnit: true,
-    style: style,
-  );
-}
- -
- - - -## API - -暂无对应api - - - \ No newline at end of file