From 78289c7dc408f6458d968a855f2430ac4299c10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Tue, 12 May 2026 11:50:07 +0800 Subject: [PATCH 01/35] =?UTF-8?q?feat(calendar):=20=E6=96=B0=E5=A2=9E=20bo?= =?UTF-8?q?ttom=20=E8=87=AA=E5=AE=9A=E4=B9=89=E5=8C=BA=E5=9F=9F=E5=B9=B6?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=86=85=E7=BD=AE=E6=97=B6=E9=97=B4=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/assets/api/calendar_api.md | 8 +- .../assets/code/calendar._buildSimple.txt | 420 ++++++++++++++++-- .../example/lib/page/t_calendar_page.dart | 420 ++++++++++++++++-- .../calendar/date_picker_model.dart | 204 --------- .../src/components/calendar/t_calendar.dart | 348 +++++++-------- .../components/calendar/t_date_picker.dart | 152 ------- .../lib/src/theme/resource_delegate.dart | 8 +- 7 files changed, 923 insertions(+), 637 deletions(-) delete mode 100644 tdesign-component/lib/src/components/calendar/date_picker_model.dart delete mode 100644 tdesign-component/lib/src/components/calendar/t_date_picker.dart diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md index 82a806754..77f74710f 100644 --- a/tdesign-component/example/assets/api/calendar_api.md +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -34,6 +34,9 @@ | --- | --- | --- | --- | | anchorDate | DateTime? | - | 锚点日期 | | animateTo | bool? | false | 动画滚动到指定位置 | +| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,位于日历主体浮层上方,接收 (BuildContext, List\) 参数 | +| bottomExpanded | bool | true | bottom 区域是否展开,由外部 setState 控制 | +| bottomExpandedListenable | ValueListenable\? | - | bottom 区域是否展开(响应式版本,优先级高于 bottomExpanded)。传入后展开/收起会跟随该 listenable 自动播放动画 | | cellHeight | double? | 60 | 日期高度 | | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | | dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 | @@ -42,7 +45,6 @@ | firstDayOfWeek | int? | 0 | 第一天从星期几开始,默认 0 = 周日 | | format | CalendarFormat? | - | 用于格式化日期的函数,可定义日期前后的显示内容和日期样式 | | height | double? | - | 高度 | -| isTimeUnit | bool? | true | 是否显示时间单位 | | key | | - | | | maxDate | int? | - | 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认半年后 | | minDate | int? | - | 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认今天 | @@ -53,16 +55,12 @@ | onChange | void Function(List value)? | - | 选中值变化时触发 | | onHeaderClick | void Function(int index, String week)? | - | 点击周时触发 | | onMonthChange | ValueChanged? | - | 月份变化时触发 | -| pickerHeight | double? | 178 | 时间选择器List的视窗高度 | -| pickerItemCount | int? | 3 | 选择器List视窗中item个数,pickerHeight / pickerItemCount即item高度 | | showLunarInfo | bool | false | 阳历模式下是否显示农历信息作为副标题 | | style | TCalendarStyle? | - | 自定义样式 | -| timePickerModel | List? | - | 自定义时间选择器 | | title | String? | - | 标题 | | titleWidget | Widget? | - | 标题组件 | | type | CalendarType? | CalendarType.single | 日历的选择类型,single = 单选;multiple = 多选;range = 区间选择 | | useSafeArea | bool? | true | 是否使用安全区域,默认true | -| useTimePicker | bool? | false | 是否显示时间选择器 | | value | List? | - | 当前选择的日期(fromMillisecondsSinceEpoch),不传则默认今天,当 type = single 时数组长度为1 | | width | double? | - | 宽度 | diff --git a/tdesign-component/example/assets/code/calendar._buildSimple.txt b/tdesign-component/example/assets/code/calendar._buildSimple.txt index 8c260fc0f..00721cf2b 100644 --- a/tdesign-component/example/assets/code/calendar._buildSimple.txt +++ b/tdesign-component/example/assets/code/calendar._buildSimple.txt @@ -3,33 +3,90 @@ Widget _buildSimple(BuildContext context) { final size = MediaQuery.of(context).size; final selected = ValueNotifier>( [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]); - return ValueListenableBuilder( - valueListenable: selected, - builder: (context, value, child) { - final date = DateTime.fromMillisecondsSinceEpoch(value[0]); + // 刷新触发器:所有示例 onConfirm 后 +1,驱动 builder 重建以更新 note + final refreshTrigger = ValueNotifier(0); + + // 时间选择器 items(时 0-23、分 0-59),一次性构造避免重复创建 + final timeItems = TPickerColumns([ + [for (int i = 0; i < 24; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}时', value: i)], + [for (int i = 0; i < 60; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}分', value: i)], + ]); + final now = DateTime.now(); + // 单个选择日历和时间 - 时分 + final pickedTime = ValueNotifier>([now.hour, now.minute]); + // 区间选择日历和时间 - [[开始时, 开始分], [结束时, 结束分]] + final pickedRangeTime = ValueNotifier>>([ + [now.hour, now.minute], + [now.hour, now.minute], + ]); + final rangeTimeTab = ValueNotifier(0); + + // 单个选择日历 - 已选日期(默认无选中) + var singleSelected = []; + // 单个选择日历 - 天气面板是否展开(默认收起,点击日期/已有选中时展开) + final singleWeatherExpanded = ValueNotifier(false); + // 多个选择日历 - 已选日期(闭包捕获,在 builder 内通过 refreshTrigger 触发更新) + var multipleDates = []; + // 区间选择日历 - 已选区间 + var rangeDates = [ + DateTime.now().millisecondsSinceEpoch, + DateTime.now().add(const Duration(days: 6)).millisecondsSinceEpoch, + ]; + // 区间选择日历和时间 - 已选区间(带时分) + var rangeTimeDates = []; + + return ValueListenableBuilder( + valueListenable: refreshTrigger, + builder: (context, _, child) { + final date = DateTime.fromMillisecondsSinceEpoch(selected.value[0]); + // 多个选择 note + final multipleNote = multipleDates.isEmpty + ? '--' + : '已选 ${multipleDates.length} 天'; + // 区间选择 note + String fmtRange(int ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return '${d.month}/${d.day}'; + } + // 区间选择日历和时间 note + String fmtRangeTime(int ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return '${d.month}/${d.day} ${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}'; + } + final rangeTimeNote = rangeTimeDates.length >= 2 + ? '${fmtRangeTime(rangeTimeDates.first)} ~ ${fmtRangeTime(rangeTimeDates.last)}' + : '--'; + return TCellGroup( cells: [ TCell( title: '单个选择日历', arrow: true, - note: '${date.year}-${date.month}-${date.day}', + note: singleSelected.isEmpty + ? '--' + : () { + final d = DateTime.fromMillisecondsSinceEpoch(singleSelected.first); + return '${d.year}-${d.month}-${d.day}'; + }(), onClick: (cell) { + // 打开时:若已有选中值则默认展开天气,否则收起 + singleWeatherExpanded.value = singleSelected.isNotEmpty; TCalendarPopup( context, visible: true, onConfirm: (value) { - print('onConfirm:$value'); - selected.value = value; - }, - onClose: () { - print('onClose'); + singleSelected = value; + refreshTrigger.value++; }, + onClose: () {}, child: TCalendar( title: '请选择日期', - value: value, + value: singleSelected, height: size.height * 0.6 + 176, + bottomExpandedListenable: singleWeatherExpanded, onCellClick: (value, type, tdate) { - print('onCellClick: $value'); + // 点击日期时展开天气面板 + singleWeatherExpanded.value = true; }, onCellLongPress: (value, type, tdate) { print('onCellLongPress: $value'); @@ -40,6 +97,57 @@ Widget _buildSimple(BuildContext context) { onChange: (value) { print('onChange: $value'); }, + bottom: (context, selectedDates) { + // 随机天气数据 + final weathers = ['☀️ 晴', '⛅ 多云', '🌧️ 小雨', '⛈️ 雷阵雨', '❄️ 小雪', '🌫️ 雾']; + final windDirs = ['北风', '南风', '东风', '西风', '微风']; + final w = weathers[DateTime.now().millisecond % weathers.length]; + final temp = -5 + (DateTime.now().millisecond % 30); + final hum = 30 + (DateTime.now().millisecond % 50); + final wind = windDirs[DateTime.now().millisecond % windDirs.length]; + final windLv = 1 + (DateTime.now().millisecond % 5); + final d = selectedDates.isEmpty + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(selectedDates.first); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${d.year}-${d.month}-${d.day}', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + const SizedBox(height: 4), + Text(w, style: const TextStyle(fontSize: 22)), + ], + ), + const SizedBox(width: 24), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [const Icon(Icons.thermostat, size: 14), const SizedBox(width: 4), Text('$temp°C')]), + const SizedBox(height: 4), + Row(children: [const Icon(Icons.water_drop, size: 14), const SizedBox(width: 4), Text('$hum%')]), + const SizedBox(height: 4), + Row(children: [const Icon(Icons.air, size: 14), const SizedBox(width: 4), Text('$wind $windLv 级')]), + ], + ), + ), + ], + ), + ); + }, ), ); }, @@ -47,15 +155,76 @@ Widget _buildSimple(BuildContext context) { TCell( title: '多个选择日历', arrow: true, + note: multipleNote, onClick: (cell) { TCalendarPopup( context, visible: true, + onConfirm: (value) { + multipleDates = value; + refreshTrigger.value++; + }, child: TCalendar( title: '请选择日期', type: CalendarType.multiple, - value: [DateTime.now().millisecondsSinceEpoch], + value: multipleDates.isEmpty + ? [DateTime.now().millisecondsSinceEpoch] + : multipleDates, height: size.height * 0.6 + 176, + bottom: (context, selectedDates) { + final dates = selectedDates + .map(DateTime.fromMillisecondsSinceEpoch) + .toList() + ..sort(); + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '已选择 ${dates.length} 天', + style: const TextStyle( + fontSize: 14, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 6), + Wrap( + spacing: 8, + runSpacing: 6, + children: dates + .map((d) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: TTheme.of(context).brandColor1, + borderRadius: + BorderRadius.circular(4), + ), + child: Text( + '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}', + style: TextStyle( + fontSize: 12, + color: TTheme.of(context) + .brandColor7), + ), + )) + .toList(), + ), + ], + ), + ); + }, ), ); }, @@ -63,20 +232,108 @@ Widget _buildSimple(BuildContext context) { TCell( title: '区间选择日历', arrow: true, + note: rangeDates.length >= 2 + ? '${fmtRange(rangeDates.first)} ~ ${fmtRange(rangeDates[1])}' + : '--', onClick: (cell) { TCalendarPopup( context, visible: true, + onConfirm: (value) { + rangeDates = value; + refreshTrigger.value++; + }, child: TCalendar( title: '请选择日期区间', type: CalendarType.range, - value: [ - DateTime.now().millisecondsSinceEpoch, - DateTime.now() - .add(const Duration(days: 6)) - .millisecondsSinceEpoch, - ], + value: rangeDates, height: size.height * 0.6 + 176, + bottom: (context, selectedDates) { + String formatDate(int ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; + } + + final hasStart = selectedDates.isNotEmpty; + final hasEnd = selectedDates.length >= 2; + final days = hasEnd + ? ((selectedDates[1] - selectedDates[0]) / + (24 * 60 * 60 * 1000)) + .round() + + 1 + : (hasStart ? 1 : 0); + + Widget buildSegment(String label, String? value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: TTheme.of(context).fontGyColor3), + ), + const SizedBox(height: 2), + Text( + value ?? '--', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: value != null + ? TTheme.of(context).fontGyColor1 + : TTheme.of(context).fontGyColor3, + ), + ), + ], + ); + } + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: buildSegment('开始', + hasStart ? formatDate(selectedDates[0]) : null), + ), + Icon(Icons.arrow_forward, + size: 16, + color: TTheme.of(context).fontGyColor3), + const SizedBox(width: 12), + Expanded( + child: buildSegment('结束', + hasEnd ? formatDate(selectedDates[1]) : null), + ), + if (days > 0) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: TTheme.of(context).brandColor1, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '共 $days 天', + style: TextStyle( + fontSize: 12, + color: TTheme.of(context).brandColor7), + ), + ), + ], + ), + ); + }, ), ); }, @@ -85,25 +342,57 @@ Widget _buildSimple(BuildContext context) { title: '单个选择日历和时间', arrow: true, note: - '${date.year}-${date.month}-${date.day} ${date.hour}:${date.minute}', + '${date.year}-${date.month}-${date.day} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}', onClick: (cell) { TCalendarPopup( context, visible: true, - onConfirm: (value) { - print('onConfirm:$value'); - selected.value = value; + onConfirm: (dates) { + // 将 Picker 选中的时分合并到日期时间戳 + final merged = dates.map((ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return DateTime( + d.year, + d.month, + d.day, + pickedTime.value[0], + pickedTime.value[1], + ).millisecondsSinceEpoch; + }).toList(); + print('onConfirm:$merged'); + selected.value = merged; + refreshTrigger.value++; }, onClose: () { print('onClose'); }, child: TCalendar( title: '请选择日期和时间', - value: value, + value: selected.value, height: size.height * 0.92, - useTimePicker: true, - // pickerHeight: 100, - // pickerItemCount: 2, + bottom: (context, selectedDates) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: TPicker( + items: timeItems, + initialValue: pickedTime.value, + height: 180, + itemCount: 5, + title: '选择时间', + onChange: (v) => + pickedTime.value = List.from(v.values), + ), + ); + }, onCellClick: (value, type, tdate) { print('onCellClick: $value'); }, @@ -123,12 +412,23 @@ Widget _buildSimple(BuildContext context) { TCell( title: '区间选择日历和时间', arrow: true, + note: rangeTimeNote, onClick: (cell) { TCalendarPopup( context, visible: true, onConfirm: (value) { - print('onConfirm: $value'); + // 把开始/结束时分合并到对应日期 + final rt = pickedRangeTime.value; + final merged = [ + for (var i = 0; i < value.length; i++) + DateTime.fromMillisecondsSinceEpoch(value[i]) + .copyWith(hour: rt[i][0], minute: rt[i][1]) + .millisecondsSinceEpoch, + ]; + print('onConfirm: $merged'); + rangeTimeDates = merged; + refreshTrigger.value++; }, onClose: () { print('onClose'); @@ -143,15 +443,52 @@ Widget _buildSimple(BuildContext context) { .add(const Duration(days: 3)) .millisecondsSinceEpoch, ], - useTimePicker: true, - onCellClick: (value, type, tdate) { - print('onCellClick: $value'); - }, - onCellLongPress: (value, type, tdate) { - print('onCellLongPress: $value'); - }, - onHeaderClick: (index, week) { - print('onHeaderClick: $week'); + bottom: (context, selectedDates) { + return DefaultTabController( + length: 2, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TTabBar( + height: 40, + showIndicator: true, + tabs: const [ + TTab(text: '开始时间'), + TTab(text: '结束时间'), + ], + onTap: (i) => rangeTimeTab.value = i, + ), + ValueListenableBuilder( + valueListenable: rangeTimeTab, + builder: (context, tab, _) => TPicker( + // 切换 tab 时重建,复位到对应时分 + key: ValueKey(tab), + items: timeItems, + initialValue: pickedRangeTime.value[tab], + height: 180, + itemCount: 5, + onChange: (v) { + final next = [...pickedRangeTime.value]; + next[tab] = List.from(v.values); + pickedRangeTime.value = next; + }, + ), + ), + ], + ), + ), + ); }, onChange: (value) { print('onChange: $value'); @@ -168,9 +505,10 @@ Widget _buildSimple(BuildContext context) { TCalendarPopup( context, visible: true, - onConfirm: (value) { - print('onConfirm:$value'); - selected.value = value; + onConfirm: (dates) { + print('onConfirm:$dates'); + selected.value = dates; + refreshTrigger.value++; }, onClose: () { print('onClose'); @@ -180,7 +518,7 @@ Widget _buildSimple(BuildContext context) { minDate: DateTime(2022, 1, 1).millisecondsSinceEpoch, maxDate: DateTime(2028, 2, 15).millisecondsSinceEpoch, anchorDate: DateTime(2026, 5), - value: value, + value: selected.value, height: size.height * 0.6 + 176, onCellClick: (value, type, tdate) { print('onCellClick: $value'); diff --git a/tdesign-component/example/lib/page/t_calendar_page.dart b/tdesign-component/example/lib/page/t_calendar_page.dart index b111e6c3b..16089484b 100644 --- a/tdesign-component/example/lib/page/t_calendar_page.dart +++ b/tdesign-component/example/lib/page/t_calendar_page.dart @@ -68,33 +68,90 @@ Widget _buildSimple(BuildContext context) { final size = MediaQuery.of(context).size; final selected = ValueNotifier>( [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]); - return ValueListenableBuilder( - valueListenable: selected, - builder: (context, value, child) { - final date = DateTime.fromMillisecondsSinceEpoch(value[0]); + // 刷新触发器:所有示例 onConfirm 后 +1,驱动 builder 重建以更新 note + final refreshTrigger = ValueNotifier(0); + + // 时间选择器 items(时 0-23、分 0-59),一次性构造避免重复创建 + final timeItems = TPickerColumns([ + [for (int i = 0; i < 24; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}时', value: i)], + [for (int i = 0; i < 60; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}分', value: i)], + ]); + final now = DateTime.now(); + // 单个选择日历和时间 - 时分 + final pickedTime = ValueNotifier>([now.hour, now.minute]); + // 区间选择日历和时间 - [[开始时, 开始分], [结束时, 结束分]] + final pickedRangeTime = ValueNotifier>>([ + [now.hour, now.minute], + [now.hour, now.minute], + ]); + final rangeTimeTab = ValueNotifier(0); + + // 单个选择日历 - 已选日期(默认无选中) + var singleSelected = []; + // 单个选择日历 - 天气面板是否展开(默认收起,点击日期/已有选中时展开) + final singleWeatherExpanded = ValueNotifier(false); + // 多个选择日历 - 已选日期(闭包捕获,在 builder 内通过 refreshTrigger 触发更新) + var multipleDates = []; + // 区间选择日历 - 已选区间 + var rangeDates = [ + DateTime.now().millisecondsSinceEpoch, + DateTime.now().add(const Duration(days: 6)).millisecondsSinceEpoch, + ]; + // 区间选择日历和时间 - 已选区间(带时分) + var rangeTimeDates = []; + + return ValueListenableBuilder( + valueListenable: refreshTrigger, + builder: (context, _, child) { + final date = DateTime.fromMillisecondsSinceEpoch(selected.value[0]); + // 多个选择 note + final multipleNote = multipleDates.isEmpty + ? '--' + : '已选 ${multipleDates.length} 天'; + // 区间选择 note + String fmtRange(int ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return '${d.month}/${d.day}'; + } + // 区间选择日历和时间 note + String fmtRangeTime(int ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return '${d.month}/${d.day} ${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}'; + } + final rangeTimeNote = rangeTimeDates.length >= 2 + ? '${fmtRangeTime(rangeTimeDates.first)} ~ ${fmtRangeTime(rangeTimeDates.last)}' + : '--'; + return TCellGroup( cells: [ TCell( title: '单个选择日历', arrow: true, - note: '${date.year}-${date.month}-${date.day}', + note: singleSelected.isEmpty + ? '--' + : () { + final d = DateTime.fromMillisecondsSinceEpoch(singleSelected.first); + return '${d.year}-${d.month}-${d.day}'; + }(), onClick: (cell) { + // 打开时:若已有选中值则默认展开天气,否则收起 + singleWeatherExpanded.value = singleSelected.isNotEmpty; TCalendarPopup( context, visible: true, onConfirm: (value) { - print('onConfirm:$value'); - selected.value = value; - }, - onClose: () { - print('onClose'); + singleSelected = value; + refreshTrigger.value++; }, + onClose: () {}, child: TCalendar( title: '请选择日期', - value: value, + value: singleSelected, height: size.height * 0.6 + 176, + bottomExpandedListenable: singleWeatherExpanded, onCellClick: (value, type, tdate) { - print('onCellClick: $value'); + // 点击日期时展开天气面板 + singleWeatherExpanded.value = true; }, onCellLongPress: (value, type, tdate) { print('onCellLongPress: $value'); @@ -105,6 +162,57 @@ Widget _buildSimple(BuildContext context) { onChange: (value) { print('onChange: $value'); }, + bottom: (context, selectedDates) { + // 随机天气数据 + final weathers = ['☀️ 晴', '⛅ 多云', '🌧️ 小雨', '⛈️ 雷阵雨', '❄️ 小雪', '🌫️ 雾']; + final windDirs = ['北风', '南风', '东风', '西风', '微风']; + final w = weathers[DateTime.now().millisecond % weathers.length]; + final temp = -5 + (DateTime.now().millisecond % 30); + final hum = 30 + (DateTime.now().millisecond % 50); + final wind = windDirs[DateTime.now().millisecond % windDirs.length]; + final windLv = 1 + (DateTime.now().millisecond % 5); + final d = selectedDates.isEmpty + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(selectedDates.first); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${d.year}-${d.month}-${d.day}', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + const SizedBox(height: 4), + Text(w, style: const TextStyle(fontSize: 22)), + ], + ), + const SizedBox(width: 24), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [const Icon(Icons.thermostat, size: 14), const SizedBox(width: 4), Text('$temp°C')]), + const SizedBox(height: 4), + Row(children: [const Icon(Icons.water_drop, size: 14), const SizedBox(width: 4), Text('$hum%')]), + const SizedBox(height: 4), + Row(children: [const Icon(Icons.air, size: 14), const SizedBox(width: 4), Text('$wind $windLv 级')]), + ], + ), + ), + ], + ), + ); + }, ), ); }, @@ -112,15 +220,76 @@ Widget _buildSimple(BuildContext context) { TCell( title: '多个选择日历', arrow: true, + note: multipleNote, onClick: (cell) { TCalendarPopup( context, visible: true, + onConfirm: (value) { + multipleDates = value; + refreshTrigger.value++; + }, child: TCalendar( title: '请选择日期', type: CalendarType.multiple, - value: [DateTime.now().millisecondsSinceEpoch], + value: multipleDates.isEmpty + ? [DateTime.now().millisecondsSinceEpoch] + : multipleDates, height: size.height * 0.6 + 176, + bottom: (context, selectedDates) { + final dates = selectedDates + .map(DateTime.fromMillisecondsSinceEpoch) + .toList() + ..sort(); + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '已选择 ${dates.length} 天', + style: const TextStyle( + fontSize: 14, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 6), + Wrap( + spacing: 8, + runSpacing: 6, + children: dates + .map((d) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: TTheme.of(context).brandColor1, + borderRadius: + BorderRadius.circular(4), + ), + child: Text( + '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}', + style: TextStyle( + fontSize: 12, + color: TTheme.of(context) + .brandColor7), + ), + )) + .toList(), + ), + ], + ), + ); + }, ), ); }, @@ -128,20 +297,108 @@ Widget _buildSimple(BuildContext context) { TCell( title: '区间选择日历', arrow: true, + note: rangeDates.length >= 2 + ? '${fmtRange(rangeDates.first)} ~ ${fmtRange(rangeDates[1])}' + : '--', onClick: (cell) { TCalendarPopup( context, visible: true, + onConfirm: (value) { + rangeDates = value; + refreshTrigger.value++; + }, child: TCalendar( title: '请选择日期区间', type: CalendarType.range, - value: [ - DateTime.now().millisecondsSinceEpoch, - DateTime.now() - .add(const Duration(days: 6)) - .millisecondsSinceEpoch, - ], + value: rangeDates, height: size.height * 0.6 + 176, + bottom: (context, selectedDates) { + String formatDate(int ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; + } + + final hasStart = selectedDates.isNotEmpty; + final hasEnd = selectedDates.length >= 2; + final days = hasEnd + ? ((selectedDates[1] - selectedDates[0]) / + (24 * 60 * 60 * 1000)) + .round() + + 1 + : (hasStart ? 1 : 0); + + Widget buildSegment(String label, String? value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: TTheme.of(context).fontGyColor3), + ), + const SizedBox(height: 2), + Text( + value ?? '--', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: value != null + ? TTheme.of(context).fontGyColor1 + : TTheme.of(context).fontGyColor3, + ), + ), + ], + ); + } + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: buildSegment('开始', + hasStart ? formatDate(selectedDates[0]) : null), + ), + Icon(Icons.arrow_forward, + size: 16, + color: TTheme.of(context).fontGyColor3), + const SizedBox(width: 12), + Expanded( + child: buildSegment('结束', + hasEnd ? formatDate(selectedDates[1]) : null), + ), + if (days > 0) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: TTheme.of(context).brandColor1, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '共 $days 天', + style: TextStyle( + fontSize: 12, + color: TTheme.of(context).brandColor7), + ), + ), + ], + ), + ); + }, ), ); }, @@ -150,25 +407,57 @@ Widget _buildSimple(BuildContext context) { title: '单个选择日历和时间', arrow: true, note: - '${date.year}-${date.month}-${date.day} ${date.hour}:${date.minute}', + '${date.year}-${date.month}-${date.day} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}', onClick: (cell) { TCalendarPopup( context, visible: true, - onConfirm: (value) { - print('onConfirm:$value'); - selected.value = value; + onConfirm: (dates) { + // 将 Picker 选中的时分合并到日期时间戳 + final merged = dates.map((ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return DateTime( + d.year, + d.month, + d.day, + pickedTime.value[0], + pickedTime.value[1], + ).millisecondsSinceEpoch; + }).toList(); + print('onConfirm:$merged'); + selected.value = merged; + refreshTrigger.value++; }, onClose: () { print('onClose'); }, child: TCalendar( title: '请选择日期和时间', - value: value, + value: selected.value, height: size.height * 0.92, - useTimePicker: true, - // pickerHeight: 100, - // pickerItemCount: 2, + bottom: (context, selectedDates) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: TPicker( + items: timeItems, + initialValue: pickedTime.value, + height: 180, + itemCount: 5, + title: '选择时间', + onChange: (v) => + pickedTime.value = List.from(v.values), + ), + ); + }, onCellClick: (value, type, tdate) { print('onCellClick: $value'); }, @@ -188,12 +477,23 @@ Widget _buildSimple(BuildContext context) { TCell( title: '区间选择日历和时间', arrow: true, + note: rangeTimeNote, onClick: (cell) { TCalendarPopup( context, visible: true, onConfirm: (value) { - print('onConfirm: $value'); + // 把开始/结束时分合并到对应日期 + final rt = pickedRangeTime.value; + final merged = [ + for (var i = 0; i < value.length; i++) + DateTime.fromMillisecondsSinceEpoch(value[i]) + .copyWith(hour: rt[i][0], minute: rt[i][1]) + .millisecondsSinceEpoch, + ]; + print('onConfirm: $merged'); + rangeTimeDates = merged; + refreshTrigger.value++; }, onClose: () { print('onClose'); @@ -208,15 +508,52 @@ Widget _buildSimple(BuildContext context) { .add(const Duration(days: 3)) .millisecondsSinceEpoch, ], - useTimePicker: true, - onCellClick: (value, type, tdate) { - print('onCellClick: $value'); - }, - onCellLongPress: (value, type, tdate) { - print('onCellLongPress: $value'); - }, - onHeaderClick: (index, week) { - print('onHeaderClick: $week'); + bottom: (context, selectedDates) { + return DefaultTabController( + length: 2, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TTabBar( + height: 40, + showIndicator: true, + tabs: const [ + TTab(text: '开始时间'), + TTab(text: '结束时间'), + ], + onTap: (i) => rangeTimeTab.value = i, + ), + ValueListenableBuilder( + valueListenable: rangeTimeTab, + builder: (context, tab, _) => TPicker( + // 切换 tab 时重建,复位到对应时分 + key: ValueKey(tab), + items: timeItems, + initialValue: pickedRangeTime.value[tab], + height: 180, + itemCount: 5, + onChange: (v) { + final next = [...pickedRangeTime.value]; + next[tab] = List.from(v.values); + pickedRangeTime.value = next; + }, + ), + ), + ], + ), + ), + ); }, onChange: (value) { print('onChange: $value'); @@ -233,9 +570,10 @@ Widget _buildSimple(BuildContext context) { TCalendarPopup( context, visible: true, - onConfirm: (value) { - print('onConfirm:$value'); - selected.value = value; + onConfirm: (dates) { + print('onConfirm:$dates'); + selected.value = dates; + refreshTrigger.value++; }, onClose: () { print('onClose'); @@ -245,7 +583,7 @@ Widget _buildSimple(BuildContext context) { minDate: DateTime(2022, 1, 1).millisecondsSinceEpoch, maxDate: DateTime(2028, 2, 15).millisecondsSinceEpoch, anchorDate: DateTime(2026, 5), - value: value, + value: selected.value, height: size.height * 0.6 + 176, onCellClick: (value, type, tdate) { print('onCellClick: $value'); diff --git a/tdesign-component/lib/src/components/calendar/date_picker_model.dart b/tdesign-component/lib/src/components/calendar/date_picker_model.dart deleted file mode 100644 index c1d5b0c22..000000000 --- a/tdesign-component/lib/src/components/calendar/date_picker_model.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../tdesign_flutter.dart'; -import '../picker/no_wave_behavior.dart'; -import '../picker/t_item_widget.dart'; -import '../picker/t_picker_option.dart'; -import '../picker/t_picker_value.dart'; - -/// 日期选择器数据模型(供 TCalendar 内部时间选择器使用) -/// -/// 精简版,仅包含 TCalendar 时间选择器所需功能 -class DatePickerModel { - final bool useYear; - final bool useMonth; - final bool useDay; - final bool useHour; - final bool useMinute; - final bool useSecond; - final bool useWeekDay; - - /// 可选起始日期 [year, month, day, ...] - final List? dateStart; - - /// 可选结束日期 - final List? dateEnd; - - /// 默认选中的日期 [year, month, day, hour, minute, second, ...] - final List? dateInitial; - - /// 过滤选项 - final List Function(String key, List items)? filterItems; - - DatePickerModel({ - this.useYear = true, - this.useMonth = true, - this.useDay = true, - this.useHour = false, - this.useMinute = false, - this.useSecond = false, - this.useWeekDay = false, - this.dateStart, - this.dateEnd, - this.dateInitial, - this.filterItems, - }); - - /// 获取年数据列表 - List get years { - final start = (dateStart != null && dateStart!.isNotEmpty) ? dateStart![0] : 1900; - final end = (dateEnd != null && dateEnd!.isNotEmpty) ? dateEnd![0] : 2100; - return List.generate(end - start + 1, (i) => start + i); - } - - /// 获取月数据列表 - List get months => List.generate(12, (i) => i + 1); - - /// 获取日数据列表 - List days(int year, int month) { - final daysInMonth = DateTime(year, month + 1).subtract(const Duration(days: 1)).day; - return List.generate(daysInMonth, (i) => i + 1); - } - - /// 获取时数据列表 - List get hours => List.generate(24, (i) => i); - - /// 获取分数据列表 - List get minutes => List.generate(60, (i) => i); - - /// 获取秒数据列表 - List get seconds => List.generate(60, (i) => i); - - /// 获取星期数据列表 - List get weekDays => ['一', '二', '三', '四', '五', '六', '日']; - - /// 所有列的 ScrollController - late List controllers; - late List data; - - /// 命名控制器便捷访问(供 TCalendar 使用) - FixedExtentScrollController get hourFixedExtentScrollController { - int idx = 0; - if (useYear) idx++; - if (useMonth) idx++; - if (useDay) idx++; - return controllers[idx]; - } - - FixedExtentScrollController get minuteFixedExtentScrollController { - int idx = 0; - if (useYear) idx++; - if (useMonth) idx++; - if (useDay) idx++; - if (useHour) idx++; - return controllers[idx]; - } - - FixedExtentScrollController get secondFixedExtentScrollController { - int idx = 0; - if (useYear) idx++; - if (useMonth) idx++; - if (useDay) idx++; - if (useHour) idx++; - if (useMinute) idx++; - return controllers[idx]; - } - - /// 初始化 - void init() { - data = []; - controllers = []; - - if (useYear) data.add(years); - if (useMonth) data.add(months); - if (useDay) data.add([31]); // 占位,下面会刷新 - if (useHour) data.add(hours); - if (useMinute) data.add(minutes); - if (useSecond) data.add(seconds); - if (useWeekDay) data.add(weekDays); - - controllers = List.generate( - data.length, - (_) => FixedExtentScrollController(), - ); - - // 设置初始位置 - if (dateInitial != null) { - final init = dateInitial!; - for (var i = 0; i < init.length && i < controllers.length; i++) { - if (data[i].isNotEmpty) { - final idx = data[i].indexOf(init[i]); - if (idx >= 0) controllers[i].jumpToItem(idx); - } - } - } - - // 刷新日列数据(必须在 controllers 初始化之后,因为需要读取选中的年/月) - if (useDay) _refreshDays(); - } - - /// 根据当前选中值刷新日列数据 - void _refreshDays() { - // 动态计算 day 列的实际索引(前面可能没有 year / month 列) - int dayCol = 0; - if (useYear) dayCol++; - if (useMonth) dayCol++; - if (dayCol >= data.length) return; - - final yearIdx = useYear - ? controllers[0].selectedItem.clamp(0, years.length - 1) - : 0; - final monthCol = useYear ? 1 : 0; - final monthIdx = useMonth - ? controllers[monthCol].selectedItem.clamp(0, months.length - 1) - : 0; - final year = useYear ? years[yearIdx] : DateTime.now().year; - final month = useMonth ? months[monthIdx] : DateTime.now().month; - data[dayCol] = days(year, month); - } - - /// 外部调用:当年/月变化时刷新后续列 - void refreshDataAndController(int changedColumn) { - if (changedColumn == 0 && useMonth) { - // 年变化 → 刷新月 - _refreshDays(); - if (controllers.length > changedColumn + 1) controllers[changedColumn + 1].jumpToItem(0); - } - if (changedColumn == 1 && useDay) { - // 月变化 → 刷新日 - _refreshDays(); - if (controllers.length > changedColumn + 1) controllers[changedColumn + 1].jumpToItem(0); - } - } - - /// 获取当前选中值 - Map get selected { - final result = {}; - var idx = 0; - if (useYear && idx < data.length) { - result['year'] = data[idx][controllers[idx].selectedItem]; - idx++; - } - if (useMonth && idx < data.length) { - result['month'] = data[idx][controllers[idx].selectedItem]; - idx++; - } - if (useDay && idx < data.length) { - result['day'] = data[idx][controllers[idx].selectedItem]; - idx++; - } - if (useHour && idx < data.length) { - result['hour'] = data[idx][controllers[idx].selectedItem]; - idx++; - } - if (useMinute && idx < data.length) { - result['minute'] = data[idx][controllers[idx].selectedItem]; - idx++; - } - if (useSecond && idx < data.length) { - result['second'] = data[idx][controllers[idx].selectedItem]; - idx++; - } - return result; - } -} diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart index 89822da4c..c2d978af0 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart @@ -1,9 +1,8 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../../tdesign_flutter.dart'; import '../../util/context_extension.dart'; import '../../util/iterable_ext.dart'; -import 'date_picker_model.dart'; -import 't_date_picker.dart'; export 't_calendar_body.dart'; export 't_calendar_cell.dart'; @@ -15,6 +14,14 @@ export 't_lunar_date.dart'; typedef CalendarFormat = TDate? Function(TDate? day); +/// 底部自定义区域构建器 +/// [context] 当前上下文 +/// [selectedDates] 当前选中的日期列表(毫秒时间戳) +typedef CalendarBottomBuilder = Widget Function( + BuildContext context, + List selectedDates, +); + enum CalendarType { single, multiple, range } enum CalendarTrigger { closeBtn, confirmBtn, overlay } @@ -43,13 +50,11 @@ class TCalendar extends StatefulWidget { this.onCellLongPress, this.onHeaderClick, this.useSafeArea = true, - this.useTimePicker = false, - this.timePickerModel, + this.bottom, + this.bottomExpanded = true, + this.bottomExpandedListenable, this.monthTitleHeight = 22, this.monthTitleBuilder, - this.pickerHeight = 178, - this.pickerItemCount = 3, - this.isTimeUnit = true, this.animateTo = false, this.cellWidget, this.onMonthChange, @@ -130,11 +135,15 @@ class TCalendar extends StatefulWidget { /// 是否使用安全区域,默认true final bool? useSafeArea; - /// 是否显示时间选择器 - final bool? useTimePicker; + /// 底部自定义区域构建器,位于日历主体浮层上方 + final CalendarBottomBuilder? bottom; + + /// bottom 区域是否展开,默认 true + final bool bottomExpanded; - /// 自定义时间选择器 - final List? timePickerModel; + /// bottom 区域是否展开(响应式版本,优先级高于 [bottomExpanded])。 + /// 传入后,bottom 展开/收起会跟随该 listenable 变化自动播放动画。 + final ValueListenable? bottomExpandedListenable; /// 月标题高度 final double? monthTitleHeight; @@ -145,15 +154,6 @@ class TCalendar extends StatefulWidget { DateTime monthDate, )? monthTitleBuilder; - /// 时间选择器List的视窗高度 - final double? pickerHeight; - - /// 选择器List视窗中item个数,pickerHeight / pickerItemCount即item高度 - final int? pickerItemCount; - - /// 是否显示时间单位 - final bool? isTimeUnit; - /// 动画滚动到指定位置 final bool? animateTo; @@ -178,10 +178,6 @@ class TCalendar extends StatefulWidget { return DateTime(date.year, date.month, date.day); }).toList(); - List? get _valueTime => value?.map((item) { - return DateTime.fromMillisecondsSinceEpoch(item); - }).toList(); - @override _TCalendarState createState() => _TCalendarState(); } @@ -191,7 +187,10 @@ class _TCalendarState extends State { late List monthNames; late TCalendarInherited? inherited; late TCalendarStyle _style; - final List timePickerModelList = []; + + /// bottom 展开时日历主体固定上移高度 + static const double _bottomOffset = 30.0; + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -225,192 +224,162 @@ class _TCalendarState extends State { Widget build(BuildContext context) { inherited = TCalendarInherited.of(context); _initValue(); - timePickerModelList.clear(); final verticalGap = _style.verticalGap ?? TTheme.of(context).spacer8; + final hasBottom = widget.bottom != null; + + if (widget.bottomExpandedListenable != null) { + // 响应式:监听 listenable 变化,让 padding 与 bottom 区动画同步重建 + return ValueListenableBuilder( + valueListenable: widget.bottomExpandedListenable!, + builder: (context, expanded, _) { + return _buildBody(context, verticalGap, hasBottom, expanded); + }, + ); + } + return _buildBody(context, verticalGap, hasBottom, widget.bottomExpanded); + } + + Widget _buildBody( + BuildContext context, double verticalGap, bool hasBottom, bool expanded) { + final bottomPadding = hasBottom && expanded ? _bottomOffset : 0.0; + return Container( height: widget.height, width: widget.width ?? double.infinity, decoration: _style.decoration, - child: Column( + child: Stack( children: [ - TCalendarHeader( - firstDayOfWeek: widget.firstDayOfWeek ?? 0, - weekdayGap: TTheme.of(context).spacer4, - padding: TTheme.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.onHeaderClick, - ), - Expanded( - child: TCalendarBody( - type: widget.type ?? CalendarType.single, - firstDayOfWeek: widget.firstDayOfWeek ?? 0, - maxDate: widget.maxDate, - anchorDate: widget.anchorDate, - minDate: widget.minDate, - value: widget._value, - bodyPadding: _style.bodyPadding ?? TTheme.of(context).spacer16, - displayFormat: widget.displayFormat ?? 'year month', - monthNames: monthNames, - monthTitleStyle: _style.monthTitleStyle, - verticalGap: verticalGap, - cellHeight: _getEffectiveCellHeight(), - monthTitleHeight: widget.monthTitleHeight ?? 22, - monthTitleBuilder: widget.monthTitleBuilder, - animateTo: widget.animateTo ?? false, - onMonthChange: widget.onMonthChange, - dateType: widget.dateType, - dataSource: widget.dataSource, - builder: (date, dateList, data, rowIndex, colIndex) { - return TCalendarCell( - height: _getEffectiveCellHeight(), - 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, - cellWidget: widget.cellWidget, - dateType: widget.dateType, - showLunarInfo: widget.showLunarInfo, - ); - }, - ), - ), - if (widget.useTimePicker == true) _getTimePicker(), - if (inherited?.usePopup == true) - inherited?.confirmBtn ?? - Padding( - padding: widget.useSafeArea == true - ? EdgeInsets.only(top: TTheme.of(context).spacer16) - : EdgeInsets.symmetric( - vertical: TTheme.of(context).spacer16), - child: TButton( - theme: TButtonTheme.primary, - text: context.resource.confirm, - isBlock: true, - size: TButtonSize.large, - onTap: inherited?.onConfirm, + Column( + children: [ + TCalendarHeader( + firstDayOfWeek: widget.firstDayOfWeek ?? 0, + weekdayGap: TTheme.of(context).spacer4, + padding: TTheme.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.onHeaderClick, + ), + Expanded( + child: AnimatedPadding( + duration: const Duration(milliseconds: 200), + padding: EdgeInsets.only(bottom: bottomPadding), + child: TCalendarBody( + type: widget.type ?? CalendarType.single, + firstDayOfWeek: widget.firstDayOfWeek ?? 0, + maxDate: widget.maxDate, + anchorDate: widget.anchorDate, + minDate: widget.minDate, + value: widget._value, + bodyPadding: + _style.bodyPadding ?? TTheme.of(context).spacer16, + displayFormat: widget.displayFormat ?? 'year month', + monthNames: monthNames, + monthTitleStyle: _style.monthTitleStyle, + verticalGap: verticalGap, + cellHeight: _getEffectiveCellHeight(), + monthTitleHeight: widget.monthTitleHeight ?? 22, + monthTitleBuilder: widget.monthTitleBuilder, + animateTo: widget.animateTo ?? false, + onMonthChange: widget.onMonthChange, + dateType: widget.dateType, + dataSource: widget.dataSource, + builder: (date, dateList, data, rowIndex, colIndex) { + return TCalendarCell( + height: _getEffectiveCellHeight(), + 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, + cellWidget: widget.cellWidget, + dateType: widget.dateType, + showLunarInfo: widget.showLunarInfo, + ); + }, ), ), - if (widget.useSafeArea == true) - SizedBox(height: MediaQuery.of(context).padding.bottom) + ), + if (inherited?.usePopup == true) + inherited?.confirmBtn ?? + Padding( + padding: widget.useSafeArea == true + ? EdgeInsets.only(top: TTheme.of(context).spacer16) + : EdgeInsets.symmetric( + vertical: TTheme.of(context).spacer16), + child: TButton( + theme: TButtonTheme.primary, + text: context.resource.confirm, + isBlock: true, + size: TButtonSize.large, + onTap: inherited?.onConfirm, + ), + ), + if (widget.useSafeArea == true) + SizedBox(height: MediaQuery.of(context).padding.bottom) + ], + ), + if (hasBottom) _buildBottom(expanded), ], ), ); } - Widget _getTimePicker() { - final noRange = widget.type != CalendarType.range; - final now = DateTime.now(); - final valueTime = widget._valueTime; - return Container( - decoration: BoxDecoration( - color: TTheme.of(context).bgColorContainer, - 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 = TDatePicker( - title: noRange - ? context.resource.time - : index == 0 - ? context.resource.start - : context.resource.end, - leftText: '', - rightText: '', - model: timePickerModel, - pickerHeight: widget.pickerHeight ?? 178, - pickerItemCount: widget.pickerItemCount ?? 3, - isTimeUnit: widget.isTimeUnit ?? true, - onConfirm: (selected) {}, - onSelectedItemChanged: (wheelIndex, index) { - final time = _getValue(inherited?.selected.value ?? []); - inherited?.selected.value = time; - widget.onChange?.call(time); - }, - ); - timePickerModelList.add(timePickerModel); - return Expanded(child: timePicker); - }, + Widget _buildBottom(bool expanded) { + final bottomOffset = (inherited?.usePopup == true) + ? (widget.useSafeArea == true + ? MediaQuery.of(context).padding.bottom + + TTheme.of(context).spacer16 + + 48 + : TTheme.of(context).spacer16 * 2 + 48) + : (widget.useSafeArea == true + ? MediaQuery.of(context).padding.bottom + : 0.0); + + return Positioned( + left: 0, + right: 0, + bottom: bottomOffset, + child: ClipRect( + child: AnimatedSlide( + duration: const Duration(milliseconds: 200), + offset: expanded ? Offset.zero : const Offset(0, 1), + child: inherited != null + ? ValueListenableBuilder>( + valueListenable: inherited!.selected, + builder: (context, selectedDates, _) { + return widget.bottom!(context, selectedDates); + }, + ) + : widget.bottom!(context, widget.value ?? []), ), ), ); } List _getValue(List value) { - var dateValue = value.map((e) { + return 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(); - if (widget.type == CalendarType.range && dateValue.length == 1) { - dateValue.add(dateValue.first); - } - 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() { @@ -421,7 +390,6 @@ class _TCalendarState extends State { inherited!.selected.value = _getValue(widget.value ?? []); }); } - /// 获取有效的单元格高度 /// 当显示农历信息时,需要更大的高度以容纳额外的文本 double _getEffectiveCellHeight() { diff --git a/tdesign-component/lib/src/components/calendar/t_date_picker.dart b/tdesign-component/lib/src/components/calendar/t_date_picker.dart deleted file mode 100644 index 57dba918b..000000000 --- a/tdesign-component/lib/src/components/calendar/t_date_picker.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../tdesign_flutter.dart'; -import '../picker/no_wave_behavior.dart'; -import '../../util/context_extension.dart'; -import 'date_picker_model.dart'; - -/// 日期/时间选择器(供 TCalendar 内部使用) -/// -/// 精简版,仅提供 TCalendar 时间选择器所需功能 -class TDatePicker extends StatefulWidget { - final String? title; - final String? leftText; - final String? rightText; - final DatePickerModel model; - final double? pickerHeight; - final int? pickerItemCount; - final bool? isTimeUnit; - final void Function(Map)? onConfirm; - final void Function(int wheelIndex, int index)? onSelectedItemChanged; - - const TDatePicker({ - super.key, - this.title, - this.leftText, - this.rightText, - required this.model, - this.pickerHeight, - this.pickerItemCount, - this.isTimeUnit, - this.onConfirm, - this.onSelectedItemChanged, - }); - - @override - State createState() => _TDatePickerState(); -} - -class _TDatePickerState extends State { - late double _pickerHeight; - - @override - void initState() { - super.initState(); - _pickerHeight = widget.pickerHeight ?? 178; - widget.model.init(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.title != null) - Padding( - padding: EdgeInsets.symmetric(horizontal: TTheme.of(context).spacer16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: () => Navigator.pop(context), - child: Text(widget.leftText ?? context.resource.cancel, style: TextStyle(color: TTheme.of(context).textColorSecondary)), - ), - Text(widget.title ?? '', style: TextStyle(fontWeight: FontWeight.w600)), - GestureDetector( - onTap: () { - widget.onConfirm?.call(widget.model.selected); - Navigator.pop(context); - }, - child: Text(widget.rightText ?? context.resource.confirm, style: TextStyle(color: TTheme.of(context).brandNormalColor)), - ), - ], - ), - ), - SizedBox( - height: _pickerHeight, - width: double.infinity, - child: Stack( - alignment: Alignment.center, - children: [ - Positioned( - top: (_pickerHeight - 40) / 2, - left: 16, - right: 16, - child: Container( - height: 40, - decoration: BoxDecoration( - color: TTheme.of(context).bgColorSecondaryContainer, - borderRadius: BorderRadius.circular(TTheme.of(context).radiusDefault), - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Row( - children: List.generate( - widget.model.controllers.length, - (i) => Expanded(child: _buildColumn(i)), - ), - ), - ), - ], - ), - ), - ], - ); - } - - Widget _buildColumn(int colIndex) { - final data = widget.model.data[colIndex]; - if (data.isEmpty) return const SizedBox.shrink(); - - return MediaQuery.removePadding( - context: context, - removeTop: true, - child: ScrollConfiguration( - behavior: NoWaveBehavior(), - child: ListWheelScrollView.useDelegate( - itemExtent: _pickerHeight / (widget.pickerItemCount ?? 5), - diameterRatio: 100, - controller: widget.model.controllers[colIndex], - physics: const FixedExtentScrollPhysics(), - onSelectedItemChanged: (index) { - // 联动刷新 - if (colIndex < widget.model.data.length - 1) { - widget.model.refreshDataAndController(colIndex); - } - widget.onSelectedItemChanged?.call(colIndex, index); - }, - childDelegate: ListWheelChildBuilderDelegate( - childCount: data.length, - builder: (context, index) { - final content = data[index].toString(); - return Container( - alignment: Alignment.center, - height: _pickerHeight / (widget.pickerItemCount ?? 5), - width: double.infinity, - child: TItemWidget( - content: content, - fixedExtentScrollController: widget.model.controllers[colIndex], - colIndex: colIndex, - index: index, - itemHeight: _pickerHeight / (widget.pickerItemCount ?? 5), - ), - ); - }, - ), - ), - ), - ); - } -} diff --git a/tdesign-component/lib/src/theme/resource_delegate.dart b/tdesign-component/lib/src/theme/resource_delegate.dart index 3119544e0..f0a020a12 100644 --- a/tdesign-component/lib/src/theme/resource_delegate.dart +++ b/tdesign-component/lib/src/theme/resource_delegate.dart @@ -109,16 +109,16 @@ abstract class TResourceDelegate { /// [TTimeCounter] 毫秒 String get milliseconds; - /// [TDatePicker] 年 + /// 年 String get yearLabel; - /// [TDatePicker] 月 + /// 月 String get monthLabel; - /// [TDatePicker] 日 + /// 日 String get dateLabel; - /// [TDatePicker] 周 + /// 周 String get weeksLabel; /// [TCalendarHeader] 星期日 From 31ee7de722980fabf1cde34410423ec60dc59d46 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 04:00:16 +0000 Subject: [PATCH 02/35] [autofix.ci] apply automated fixes --- .../example/assets/api/calendar_api.md | 6 +- tdesign-site/src/calendar/README.md | 428 ++++++++++++++++-- 2 files changed, 385 insertions(+), 49 deletions(-) diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md index 77f74710f..763fa6b48 100644 --- a/tdesign-component/example/assets/api/calendar_api.md +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -34,9 +34,9 @@ | --- | --- | --- | --- | | anchorDate | DateTime? | - | 锚点日期 | | animateTo | bool? | false | 动画滚动到指定位置 | -| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,位于日历主体浮层上方,接收 (BuildContext, List\) 参数 | -| bottomExpanded | bool | true | bottom 区域是否展开,由外部 setState 控制 | -| bottomExpandedListenable | ValueListenable\? | - | bottom 区域是否展开(响应式版本,优先级高于 bottomExpanded)。传入后展开/收起会跟随该 listenable 自动播放动画 | +| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,位于日历主体浮层上方 | +| bottomExpanded | bool | true | bottom 区域是否展开,默认 true | +| bottomExpandedListenable | ValueListenable? | - | bottom 区域是否展开(响应式版本,优先级高于 [bottomExpanded])。 | | cellHeight | double? | 60 | 日期高度 | | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | | dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 | diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md index c4577e694..c1b3d87f3 100644 --- a/tdesign-site/src/calendar/README.md +++ b/tdesign-site/src/calendar/README.md @@ -30,33 +30,90 @@ Widget _buildSimple(BuildContext context) { final size = MediaQuery.of(context).size; final selected = ValueNotifier>( [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]); - return ValueListenableBuilder( - valueListenable: selected, - builder: (context, value, child) { - final date = DateTime.fromMillisecondsSinceEpoch(value[0]); + // 刷新触发器:所有示例 onConfirm 后 +1,驱动 builder 重建以更新 note + final refreshTrigger = ValueNotifier(0); + + // 时间选择器 items(时 0-23、分 0-59),一次性构造避免重复创建 + final timeItems = TPickerColumns([ + [for (int i = 0; i < 24; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}时', value: i)], + [for (int i = 0; i < 60; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}分', value: i)], + ]); + final now = DateTime.now(); + // 单个选择日历和时间 - 时分 + final pickedTime = ValueNotifier>([now.hour, now.minute]); + // 区间选择日历和时间 - [[开始时, 开始分], [结束时, 结束分]] + final pickedRangeTime = ValueNotifier>>([ + [now.hour, now.minute], + [now.hour, now.minute], + ]); + final rangeTimeTab = ValueNotifier(0); + + // 单个选择日历 - 已选日期(默认无选中) + var singleSelected = []; + // 单个选择日历 - 天气面板是否展开(默认收起,点击日期/已有选中时展开) + final singleWeatherExpanded = ValueNotifier(false); + // 多个选择日历 - 已选日期(闭包捕获,在 builder 内通过 refreshTrigger 触发更新) + var multipleDates = []; + // 区间选择日历 - 已选区间 + var rangeDates = [ + DateTime.now().millisecondsSinceEpoch, + DateTime.now().add(const Duration(days: 6)).millisecondsSinceEpoch, + ]; + // 区间选择日历和时间 - 已选区间(带时分) + var rangeTimeDates = []; + + return ValueListenableBuilder( + valueListenable: refreshTrigger, + builder: (context, _, child) { + final date = DateTime.fromMillisecondsSinceEpoch(selected.value[0]); + // 多个选择 note + final multipleNote = multipleDates.isEmpty + ? '--' + : '已选 ${multipleDates.length} 天'; + // 区间选择 note + String fmtRange(int ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return '${d.month}/${d.day}'; + } + // 区间选择日历和时间 note + String fmtRangeTime(int ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return '${d.month}/${d.day} ${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}'; + } + final rangeTimeNote = rangeTimeDates.length >= 2 + ? '${fmtRangeTime(rangeTimeDates.first)} ~ ${fmtRangeTime(rangeTimeDates.last)}' + : '--'; + return TCellGroup( cells: [ TCell( title: '单个选择日历', arrow: true, - note: '${date.year}-${date.month}-${date.day}', + note: singleSelected.isEmpty + ? '--' + : () { + final d = DateTime.fromMillisecondsSinceEpoch(singleSelected.first); + return '${d.year}-${d.month}-${d.day}'; + }(), onClick: (cell) { + // 打开时:若已有选中值则默认展开天气,否则收起 + singleWeatherExpanded.value = singleSelected.isNotEmpty; TCalendarPopup( context, visible: true, onConfirm: (value) { - print('onConfirm:$value'); - selected.value = value; - }, - onClose: () { - print('onClose'); + singleSelected = value; + refreshTrigger.value++; }, + onClose: () {}, child: TCalendar( title: '请选择日期', - value: value, + value: singleSelected, height: size.height * 0.6 + 176, + bottomExpandedListenable: singleWeatherExpanded, onCellClick: (value, type, tdate) { - print('onCellClick: $value'); + // 点击日期时展开天气面板 + singleWeatherExpanded.value = true; }, onCellLongPress: (value, type, tdate) { print('onCellLongPress: $value'); @@ -67,6 +124,57 @@ Widget _buildSimple(BuildContext context) { onChange: (value) { print('onChange: $value'); }, + bottom: (context, selectedDates) { + // 随机天气数据 + final weathers = ['☀️ 晴', '⛅ 多云', '🌧️ 小雨', '⛈️ 雷阵雨', '❄️ 小雪', '🌫️ 雾']; + final windDirs = ['北风', '南风', '东风', '西风', '微风']; + final w = weathers[DateTime.now().millisecond % weathers.length]; + final temp = -5 + (DateTime.now().millisecond % 30); + final hum = 30 + (DateTime.now().millisecond % 50); + final wind = windDirs[DateTime.now().millisecond % windDirs.length]; + final windLv = 1 + (DateTime.now().millisecond % 5); + final d = selectedDates.isEmpty + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(selectedDates.first); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${d.year}-${d.month}-${d.day}', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + const SizedBox(height: 4), + Text(w, style: const TextStyle(fontSize: 22)), + ], + ), + const SizedBox(width: 24), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [const Icon(Icons.thermostat, size: 14), const SizedBox(width: 4), Text('$temp°C')]), + const SizedBox(height: 4), + Row(children: [const Icon(Icons.water_drop, size: 14), const SizedBox(width: 4), Text('$hum%')]), + const SizedBox(height: 4), + Row(children: [const Icon(Icons.air, size: 14), const SizedBox(width: 4), Text('$wind $windLv 级')]), + ], + ), + ), + ], + ), + ); + }, ), ); }, @@ -74,15 +182,76 @@ Widget _buildSimple(BuildContext context) { TCell( title: '多个选择日历', arrow: true, + note: multipleNote, onClick: (cell) { TCalendarPopup( context, visible: true, + onConfirm: (value) { + multipleDates = value; + refreshTrigger.value++; + }, child: TCalendar( title: '请选择日期', type: CalendarType.multiple, - value: [DateTime.now().millisecondsSinceEpoch], + value: multipleDates.isEmpty + ? [DateTime.now().millisecondsSinceEpoch] + : multipleDates, height: size.height * 0.6 + 176, + bottom: (context, selectedDates) { + final dates = selectedDates + .map(DateTime.fromMillisecondsSinceEpoch) + .toList() + ..sort(); + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '已选择 ${dates.length} 天', + style: const TextStyle( + fontSize: 14, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 6), + Wrap( + spacing: 8, + runSpacing: 6, + children: dates + .map((d) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: TTheme.of(context).brandColor1, + borderRadius: + BorderRadius.circular(4), + ), + child: Text( + '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}', + style: TextStyle( + fontSize: 12, + color: TTheme.of(context) + .brandColor7), + ), + )) + .toList(), + ), + ], + ), + ); + }, ), ); }, @@ -90,20 +259,108 @@ Widget _buildSimple(BuildContext context) { TCell( title: '区间选择日历', arrow: true, + note: rangeDates.length >= 2 + ? '${fmtRange(rangeDates.first)} ~ ${fmtRange(rangeDates[1])}' + : '--', onClick: (cell) { TCalendarPopup( context, visible: true, + onConfirm: (value) { + rangeDates = value; + refreshTrigger.value++; + }, child: TCalendar( title: '请选择日期区间', type: CalendarType.range, - value: [ - DateTime.now().millisecondsSinceEpoch, - DateTime.now() - .add(const Duration(days: 6)) - .millisecondsSinceEpoch, - ], + value: rangeDates, height: size.height * 0.6 + 176, + bottom: (context, selectedDates) { + String formatDate(int ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; + } + + final hasStart = selectedDates.isNotEmpty; + final hasEnd = selectedDates.length >= 2; + final days = hasEnd + ? ((selectedDates[1] - selectedDates[0]) / + (24 * 60 * 60 * 1000)) + .round() + + 1 + : (hasStart ? 1 : 0); + + Widget buildSegment(String label, String? value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: TTheme.of(context).fontGyColor3), + ), + const SizedBox(height: 2), + Text( + value ?? '--', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: value != null + ? TTheme.of(context).fontGyColor1 + : TTheme.of(context).fontGyColor3, + ), + ), + ], + ); + } + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: buildSegment('开始', + hasStart ? formatDate(selectedDates[0]) : null), + ), + Icon(Icons.arrow_forward, + size: 16, + color: TTheme.of(context).fontGyColor3), + const SizedBox(width: 12), + Expanded( + child: buildSegment('结束', + hasEnd ? formatDate(selectedDates[1]) : null), + ), + if (days > 0) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: TTheme.of(context).brandColor1, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '共 $days 天', + style: TextStyle( + fontSize: 12, + color: TTheme.of(context).brandColor7), + ), + ), + ], + ), + ); + }, ), ); }, @@ -112,25 +369,57 @@ Widget _buildSimple(BuildContext context) { title: '单个选择日历和时间', arrow: true, note: - '${date.year}-${date.month}-${date.day} ${date.hour}:${date.minute}', + '${date.year}-${date.month}-${date.day} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}', onClick: (cell) { TCalendarPopup( context, visible: true, - onConfirm: (value) { - print('onConfirm:$value'); - selected.value = value; + onConfirm: (dates) { + // 将 Picker 选中的时分合并到日期时间戳 + final merged = dates.map((ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return DateTime( + d.year, + d.month, + d.day, + pickedTime.value[0], + pickedTime.value[1], + ).millisecondsSinceEpoch; + }).toList(); + print('onConfirm:$merged'); + selected.value = merged; + refreshTrigger.value++; }, onClose: () { print('onClose'); }, child: TCalendar( title: '请选择日期和时间', - value: value, + value: selected.value, height: size.height * 0.92, - useTimePicker: true, - // pickerHeight: 100, - // pickerItemCount: 2, + bottom: (context, selectedDates) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: TPicker( + items: timeItems, + initialValue: pickedTime.value, + height: 180, + itemCount: 5, + title: '选择时间', + onChange: (v) => + pickedTime.value = List.from(v.values), + ), + ); + }, onCellClick: (value, type, tdate) { print('onCellClick: $value'); }, @@ -150,12 +439,23 @@ Widget _buildSimple(BuildContext context) { TCell( title: '区间选择日历和时间', arrow: true, + note: rangeTimeNote, onClick: (cell) { TCalendarPopup( context, visible: true, onConfirm: (value) { - print('onConfirm: $value'); + // 把开始/结束时分合并到对应日期 + final rt = pickedRangeTime.value; + final merged = [ + for (var i = 0; i < value.length; i++) + DateTime.fromMillisecondsSinceEpoch(value[i]) + .copyWith(hour: rt[i][0], minute: rt[i][1]) + .millisecondsSinceEpoch, + ]; + print('onConfirm: $merged'); + rangeTimeDates = merged; + refreshTrigger.value++; }, onClose: () { print('onClose'); @@ -170,15 +470,52 @@ Widget _buildSimple(BuildContext context) { .add(const Duration(days: 3)) .millisecondsSinceEpoch, ], - useTimePicker: true, - onCellClick: (value, type, tdate) { - print('onCellClick: $value'); - }, - onCellLongPress: (value, type, tdate) { - print('onCellLongPress: $value'); - }, - onHeaderClick: (index, week) { - print('onHeaderClick: $week'); + bottom: (context, selectedDates) { + return DefaultTabController( + length: 2, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TTabBar( + height: 40, + showIndicator: true, + tabs: const [ + TTab(text: '开始时间'), + TTab(text: '结束时间'), + ], + onTap: (i) => rangeTimeTab.value = i, + ), + ValueListenableBuilder( + valueListenable: rangeTimeTab, + builder: (context, tab, _) => TPicker( + // 切换 tab 时重建,复位到对应时分 + key: ValueKey(tab), + items: timeItems, + initialValue: pickedRangeTime.value[tab], + height: 180, + itemCount: 5, + onChange: (v) { + final next = [...pickedRangeTime.value]; + next[tab] = List.from(v.values); + pickedRangeTime.value = next; + }, + ), + ), + ], + ), + ), + ); }, onChange: (value) { print('onChange: $value'); @@ -195,9 +532,10 @@ Widget _buildSimple(BuildContext context) { TCalendarPopup( context, visible: true, - onConfirm: (value) { - print('onConfirm:$value'); - selected.value = value; + onConfirm: (dates) { + print('onConfirm:$dates'); + selected.value = dates; + refreshTrigger.value++; }, onClose: () { print('onClose'); @@ -207,7 +545,7 @@ Widget _buildSimple(BuildContext context) { minDate: DateTime(2022, 1, 1).millisecondsSinceEpoch, maxDate: DateTime(2028, 2, 15).millisecondsSinceEpoch, anchorDate: DateTime(2026, 5), - value: value, + value: selected.value, height: size.height * 0.6 + 176, onCellClick: (value, type, tdate) { print('onCellClick: $value'); @@ -1480,6 +1818,9 @@ Widget _buildLunar(BuildContext context) { | --- | --- | --- | --- | | anchorDate | DateTime? | - | 锚点日期 | | animateTo | bool? | false | 动画滚动到指定位置 | +| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,位于日历主体浮层上方 | +| bottomExpanded | bool | true | bottom 区域是否展开,默认 true | +| bottomExpandedListenable | ValueListenable? | - | bottom 区域是否展开(响应式版本,优先级高于 [bottomExpanded])。 | | cellHeight | double? | 60 | 日期高度 | | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | | dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 | @@ -1488,7 +1829,6 @@ Widget _buildLunar(BuildContext context) { | firstDayOfWeek | int? | 0 | 第一天从星期几开始,默认 0 = 周日 | | format | CalendarFormat? | - | 用于格式化日期的函数,可定义日期前后的显示内容和日期样式 | | height | double? | - | 高度 | -| isTimeUnit | bool? | true | 是否显示时间单位 | | key | | - | | | maxDate | int? | - | 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认半年后 | | minDate | int? | - | 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认今天 | @@ -1499,16 +1839,12 @@ Widget _buildLunar(BuildContext context) { | onChange | void Function(List value)? | - | 选中值变化时触发 | | onHeaderClick | void Function(int index, String week)? | - | 点击周时触发 | | onMonthChange | ValueChanged? | - | 月份变化时触发 | -| pickerHeight | double? | 178 | 时间选择器List的视窗高度 | -| pickerItemCount | int? | 3 | 选择器List视窗中item个数,pickerHeight / pickerItemCount即item高度 | | showLunarInfo | bool | false | 阳历模式下是否显示农历信息作为副标题 | | style | TCalendarStyle? | - | 自定义样式 | -| timePickerModel | List? | - | 自定义时间选择器 | | title | String? | - | 标题 | | titleWidget | Widget? | - | 标题组件 | | type | CalendarType? | CalendarType.single | 日历的选择类型,single = 单选;multiple = 多选;range = 区间选择 | | useSafeArea | bool? | true | 是否使用安全区域,默认true | -| useTimePicker | bool? | false | 是否显示时间选择器 | | value | List? | - | 当前选择的日期(fromMillisecondsSinceEpoch),不传则默认今天,当 type = single 时数组长度为1 | | width | double? | - | 宽度 | From 7bb7747ae90c91fa865f0ca8046165138d35b22b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Tue, 12 May 2026 15:51:05 +0800 Subject: [PATCH 03/35] =?UTF-8?q?refactor(calendar):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=E4=BB=A3=E7=A0=81=E5=B9=B6=E5=90=88=E5=B9=B6?= =?UTF-8?q?bottomExpanded=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/assets/api/calendar_api.md | 3 +- .../assets/code/calendar._buildSimple.txt | 541 +------- .../example/lib/page/t_calendar_page.dart | 1203 ++++++++++------- .../src/components/calendar/t_calendar.dart | 266 ++-- 4 files changed, 848 insertions(+), 1165 deletions(-) diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md index 763fa6b48..e6bfea320 100644 --- a/tdesign-component/example/assets/api/calendar_api.md +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -35,8 +35,7 @@ | anchorDate | DateTime? | - | 锚点日期 | | animateTo | bool? | false | 动画滚动到指定位置 | | bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,位于日历主体浮层上方 | -| bottomExpanded | bool | true | bottom 区域是否展开,默认 true | -| bottomExpandedListenable | ValueListenable? | - | bottom 区域是否展开(响应式版本,优先级高于 [bottomExpanded])。 | +| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。传 null 表示始终展开;传 ValueNotifier 时展开/收起会跟随 listenable 变化播放滑动动画 | | cellHeight | double? | 60 | 日期高度 | | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | | dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 | diff --git a/tdesign-component/example/assets/code/calendar._buildSimple.txt b/tdesign-component/example/assets/code/calendar._buildSimple.txt index 00721cf2b..58d96d7de 100644 --- a/tdesign-component/example/assets/code/calendar._buildSimple.txt +++ b/tdesign-component/example/assets/code/calendar._buildSimple.txt @@ -1,543 +1,4 @@ Widget _buildSimple(BuildContext context) { - final size = MediaQuery.of(context).size; - final selected = ValueNotifier>( - [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]); - // 刷新触发器:所有示例 onConfirm 后 +1,驱动 builder 重建以更新 note - final refreshTrigger = ValueNotifier(0); - - // 时间选择器 items(时 0-23、分 0-59),一次性构造避免重复创建 - final timeItems = TPickerColumns([ - [for (int i = 0; i < 24; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}时', value: i)], - [for (int i = 0; i < 60; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}分', value: i)], - ]); - final now = DateTime.now(); - // 单个选择日历和时间 - 时分 - final pickedTime = ValueNotifier>([now.hour, now.minute]); - // 区间选择日历和时间 - [[开始时, 开始分], [结束时, 结束分]] - final pickedRangeTime = ValueNotifier>>([ - [now.hour, now.minute], - [now.hour, now.minute], - ]); - final rangeTimeTab = ValueNotifier(0); - - // 单个选择日历 - 已选日期(默认无选中) - var singleSelected = []; - // 单个选择日历 - 天气面板是否展开(默认收起,点击日期/已有选中时展开) - final singleWeatherExpanded = ValueNotifier(false); - // 多个选择日历 - 已选日期(闭包捕获,在 builder 内通过 refreshTrigger 触发更新) - var multipleDates = []; - // 区间选择日历 - 已选区间 - var rangeDates = [ - DateTime.now().millisecondsSinceEpoch, - DateTime.now().add(const Duration(days: 6)).millisecondsSinceEpoch, - ]; - // 区间选择日历和时间 - 已选区间(带时分) - var rangeTimeDates = []; - - return ValueListenableBuilder( - valueListenable: refreshTrigger, - builder: (context, _, child) { - final date = DateTime.fromMillisecondsSinceEpoch(selected.value[0]); - // 多个选择 note - final multipleNote = multipleDates.isEmpty - ? '--' - : '已选 ${multipleDates.length} 天'; - // 区间选择 note - String fmtRange(int ms) { - final d = DateTime.fromMillisecondsSinceEpoch(ms); - return '${d.month}/${d.day}'; - } - // 区间选择日历和时间 note - String fmtRangeTime(int ms) { - final d = DateTime.fromMillisecondsSinceEpoch(ms); - return '${d.month}/${d.day} ${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}'; - } - final rangeTimeNote = rangeTimeDates.length >= 2 - ? '${fmtRangeTime(rangeTimeDates.first)} ~ ${fmtRangeTime(rangeTimeDates.last)}' - : '--'; - - return TCellGroup( - cells: [ - TCell( - title: '单个选择日历', - arrow: true, - note: singleSelected.isEmpty - ? '--' - : () { - final d = DateTime.fromMillisecondsSinceEpoch(singleSelected.first); - return '${d.year}-${d.month}-${d.day}'; - }(), - onClick: (cell) { - // 打开时:若已有选中值则默认展开天气,否则收起 - singleWeatherExpanded.value = singleSelected.isNotEmpty; - TCalendarPopup( - context, - visible: true, - onConfirm: (value) { - singleSelected = value; - refreshTrigger.value++; - }, - onClose: () {}, - child: TCalendar( - title: '请选择日期', - value: singleSelected, - height: size.height * 0.6 + 176, - bottomExpandedListenable: singleWeatherExpanded, - onCellClick: (value, type, tdate) { - // 点击日期时展开天气面板 - singleWeatherExpanded.value = true; - }, - onCellLongPress: (value, type, tdate) { - print('onCellLongPress: $value'); - }, - onHeaderClick: (index, week) { - print('onHeaderClick: $week'); - }, - onChange: (value) { - print('onChange: $value'); - }, - bottom: (context, selectedDates) { - // 随机天气数据 - final weathers = ['☀️ 晴', '⛅ 多云', '🌧️ 小雨', '⛈️ 雷阵雨', '❄️ 小雪', '🌫️ 雾']; - final windDirs = ['北风', '南风', '东风', '西风', '微风']; - final w = weathers[DateTime.now().millisecond % weathers.length]; - final temp = -5 + (DateTime.now().millisecond % 30); - final hum = 30 + (DateTime.now().millisecond % 50); - final wind = windDirs[DateTime.now().millisecond % windDirs.length]; - final windLv = 1 + (DateTime.now().millisecond % 5); - final d = selectedDates.isEmpty - ? DateTime.now() - : DateTime.fromMillisecondsSinceEpoch(selectedDates.first); - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: const [ - BoxShadow( - color: Color.fromRGBO(0, 0, 0, 0.04), - blurRadius: 12, - offset: Offset(0, -2), - ), - ], - ), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('${d.year}-${d.month}-${d.day}', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), - const SizedBox(height: 4), - Text(w, style: const TextStyle(fontSize: 22)), - ], - ), - const SizedBox(width: 24), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [const Icon(Icons.thermostat, size: 14), const SizedBox(width: 4), Text('$temp°C')]), - const SizedBox(height: 4), - Row(children: [const Icon(Icons.water_drop, size: 14), const SizedBox(width: 4), Text('$hum%')]), - const SizedBox(height: 4), - Row(children: [const Icon(Icons.air, size: 14), const SizedBox(width: 4), Text('$wind $windLv 级')]), - ], - ), - ), - ], - ), - ); - }, - ), - ); - }, - ), - TCell( - title: '多个选择日历', - arrow: true, - note: multipleNote, - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - onConfirm: (value) { - multipleDates = value; - refreshTrigger.value++; - }, - child: TCalendar( - title: '请选择日期', - type: CalendarType.multiple, - value: multipleDates.isEmpty - ? [DateTime.now().millisecondsSinceEpoch] - : multipleDates, - height: size.height * 0.6 + 176, - bottom: (context, selectedDates) { - final dates = selectedDates - .map(DateTime.fromMillisecondsSinceEpoch) - .toList() - ..sort(); - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: const [ - BoxShadow( - color: Color.fromRGBO(0, 0, 0, 0.04), - blurRadius: 12, - offset: Offset(0, -2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '已选择 ${dates.length} 天', - style: const TextStyle( - fontSize: 14, fontWeight: FontWeight.w500), - ), - const SizedBox(height: 6), - Wrap( - spacing: 8, - runSpacing: 6, - children: dates - .map((d) => Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: TTheme.of(context).brandColor1, - borderRadius: - BorderRadius.circular(4), - ), - child: Text( - '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}', - style: TextStyle( - fontSize: 12, - color: TTheme.of(context) - .brandColor7), - ), - )) - .toList(), - ), - ], - ), - ); - }, - ), - ); - }, - ), - TCell( - title: '区间选择日历', - arrow: true, - note: rangeDates.length >= 2 - ? '${fmtRange(rangeDates.first)} ~ ${fmtRange(rangeDates[1])}' - : '--', - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - onConfirm: (value) { - rangeDates = value; - refreshTrigger.value++; - }, - child: TCalendar( - title: '请选择日期区间', - type: CalendarType.range, - value: rangeDates, - height: size.height * 0.6 + 176, - bottom: (context, selectedDates) { - String formatDate(int ms) { - final d = DateTime.fromMillisecondsSinceEpoch(ms); - return '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; - } - - final hasStart = selectedDates.isNotEmpty; - final hasEnd = selectedDates.length >= 2; - final days = hasEnd - ? ((selectedDates[1] - selectedDates[0]) / - (24 * 60 * 60 * 1000)) - .round() + - 1 - : (hasStart ? 1 : 0); - - Widget buildSegment(String label, String? value) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - fontSize: 12, - color: TTheme.of(context).fontGyColor3), - ), - const SizedBox(height: 2), - Text( - value ?? '--', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: value != null - ? TTheme.of(context).fontGyColor1 - : TTheme.of(context).fontGyColor3, - ), - ), - ], - ); - } - - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: const [ - BoxShadow( - color: Color.fromRGBO(0, 0, 0, 0.04), - blurRadius: 12, - offset: Offset(0, -2), - ), - ], - ), - child: Row( - children: [ - Expanded( - child: buildSegment('开始', - hasStart ? formatDate(selectedDates[0]) : null), - ), - Icon(Icons.arrow_forward, - size: 16, - color: TTheme.of(context).fontGyColor3), - const SizedBox(width: 12), - Expanded( - child: buildSegment('结束', - hasEnd ? formatDate(selectedDates[1]) : null), - ), - if (days > 0) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: TTheme.of(context).brandColor1, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - '共 $days 天', - style: TextStyle( - fontSize: 12, - color: TTheme.of(context).brandColor7), - ), - ), - ], - ), - ); - }, - ), - ); - }, - ), - TCell( - title: '单个选择日历和时间', - arrow: true, - note: - '${date.year}-${date.month}-${date.day} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}', - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - onConfirm: (dates) { - // 将 Picker 选中的时分合并到日期时间戳 - final merged = dates.map((ms) { - final d = DateTime.fromMillisecondsSinceEpoch(ms); - return DateTime( - d.year, - d.month, - d.day, - pickedTime.value[0], - pickedTime.value[1], - ).millisecondsSinceEpoch; - }).toList(); - print('onConfirm:$merged'); - selected.value = merged; - refreshTrigger.value++; - }, - onClose: () { - print('onClose'); - }, - child: TCalendar( - title: '请选择日期和时间', - value: selected.value, - height: size.height * 0.92, - bottom: (context, selectedDates) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: const [ - BoxShadow( - color: Color.fromRGBO(0, 0, 0, 0.04), - blurRadius: 12, - offset: Offset(0, -2), - ), - ], - ), - child: TPicker( - items: timeItems, - initialValue: pickedTime.value, - height: 180, - itemCount: 5, - title: '选择时间', - onChange: (v) => - pickedTime.value = List.from(v.values), - ), - ); - }, - onCellClick: (value, type, tdate) { - print('onCellClick: $value'); - }, - onCellLongPress: (value, type, tdate) { - print('onCellLongPress: $value'); - }, - onHeaderClick: (index, week) { - print('onHeaderClick: $week'); - }, - onChange: (value) { - print('onChange: $value'); - }, - ), - ); - }, - ), - TCell( - title: '区间选择日历和时间', - arrow: true, - note: rangeTimeNote, - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - onConfirm: (value) { - // 把开始/结束时分合并到对应日期 - final rt = pickedRangeTime.value; - final merged = [ - for (var i = 0; i < value.length; i++) - DateTime.fromMillisecondsSinceEpoch(value[i]) - .copyWith(hour: rt[i][0], minute: rt[i][1]) - .millisecondsSinceEpoch, - ]; - print('onConfirm: $merged'); - rangeTimeDates = merged; - refreshTrigger.value++; - }, - onClose: () { - print('onClose'); - }, - child: TCalendar( - title: '请选择日期和时间区间', - height: size.height * 0.92, - type: CalendarType.range, - value: [ - DateTime.now().millisecondsSinceEpoch, - DateTime.now() - .add(const Duration(days: 3)) - .millisecondsSinceEpoch, - ], - bottom: (context, selectedDates) { - return DefaultTabController( - length: 2, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: const [ - BoxShadow( - color: Color.fromRGBO(0, 0, 0, 0.04), - blurRadius: 12, - offset: Offset(0, -2), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TTabBar( - height: 40, - showIndicator: true, - tabs: const [ - TTab(text: '开始时间'), - TTab(text: '结束时间'), - ], - onTap: (i) => rangeTimeTab.value = i, - ), - ValueListenableBuilder( - valueListenable: rangeTimeTab, - builder: (context, tab, _) => TPicker( - // 切换 tab 时重建,复位到对应时分 - key: ValueKey(tab), - items: timeItems, - initialValue: pickedRangeTime.value[tab], - height: 180, - itemCount: 5, - onChange: (v) { - final next = [...pickedRangeTime.value]; - next[tab] = List.from(v.values); - pickedRangeTime.value = next; - }, - ), - ), - ], - ), - ), - ); - }, - onChange: (value) { - print('onChange: $value'); - }, - ), - ); - }, - ), - TCell( - title: '添加锚点', - arrow: true, - note: '${date.year}-${date.month}-${date.day}', - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - onConfirm: (dates) { - print('onConfirm:$dates'); - selected.value = dates; - refreshTrigger.value++; - }, - onClose: () { - print('onClose'); - }, - child: TCalendar( - title: '请选择日期', - minDate: DateTime(2022, 1, 1).millisecondsSinceEpoch, - maxDate: DateTime(2028, 2, 15).millisecondsSinceEpoch, - anchorDate: DateTime(2026, 5), - value: selected.value, - height: size.height * 0.6 + 176, - onCellClick: (value, type, tdate) { - print('onCellClick: $value'); - }, - onCellLongPress: (value, type, tdate) { - print('onCellLongPress: $value'); - }, - onHeaderClick: (index, week) { - print('onHeaderClick: $week'); - }, - onChange: (value) { - print('onChange: $value'); - }, - ), - ); - }, - ), - ], - ); - }, - ); + return const _SimpleDemo(); } \ No newline at end of file diff --git a/tdesign-component/example/lib/page/t_calendar_page.dart b/tdesign-component/example/lib/page/t_calendar_page.dart index 16089484b..fde0cd058 100644 --- a/tdesign-component/example/lib/page/t_calendar_page.dart +++ b/tdesign-component/example/lib/page/t_calendar_page.dart @@ -65,546 +65,705 @@ class TCalendarPage extends StatelessWidget { @Demo(group: 'calendar') Widget _buildSimple(BuildContext context) { - final size = MediaQuery.of(context).size; - final selected = ValueNotifier>( - [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]); - // 刷新触发器:所有示例 onConfirm 后 +1,驱动 builder 重建以更新 note - final refreshTrigger = ValueNotifier(0); + return const _SimpleDemo(); +} - // 时间选择器 items(时 0-23、分 0-59),一次性构造避免重复创建 - final timeItems = TPickerColumns([ - [for (int i = 0; i < 24; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}时', value: i)], - [for (int i = 0; i < 60; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}分', value: i)], - ]); - final now = DateTime.now(); - // 单个选择日历和时间 - 时分 - final pickedTime = ValueNotifier>([now.hour, now.minute]); - // 区间选择日历和时间 - [[开始时, 开始分], [结束时, 结束分]] - final pickedRangeTime = ValueNotifier>>([ - [now.hour, now.minute], - [now.hour, now.minute], - ]); - final rangeTimeTab = ValueNotifier(0); - - // 单个选择日历 - 已选日期(默认无选中) - var singleSelected = []; - // 单个选择日历 - 天气面板是否展开(默认收起,点击日期/已有选中时展开) - final singleWeatherExpanded = ValueNotifier(false); - // 多个选择日历 - 已选日期(闭包捕获,在 builder 内通过 refreshTrigger 触发更新) - var multipleDates = []; - // 区间选择日历 - 已选区间 - var rangeDates = [ - DateTime.now().millisecondsSinceEpoch, - DateTime.now().add(const Duration(days: 6)).millisecondsSinceEpoch, - ]; - // 区间选择日历和时间 - 已选区间(带时分) - var rangeTimeDates = []; - - return ValueListenableBuilder( - valueListenable: refreshTrigger, - builder: (context, _, child) { - final date = DateTime.fromMillisecondsSinceEpoch(selected.value[0]); - // 多个选择 note - final multipleNote = multipleDates.isEmpty - ? '--' - : '已选 ${multipleDates.length} 天'; - // 区间选择 note - String fmtRange(int ms) { - final d = DateTime.fromMillisecondsSinceEpoch(ms); - return '${d.month}/${d.day}'; - } - // 区间选择日历和时间 note - String fmtRangeTime(int ms) { - final d = DateTime.fromMillisecondsSinceEpoch(ms); - return '${d.month}/${d.day} ${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}'; - } - final rangeTimeNote = rangeTimeDates.length >= 2 - ? '${fmtRangeTime(rangeTimeDates.first)} ~ ${fmtRangeTime(rangeTimeDates.last)}' - : '--'; +/// 「组件类型」演示容器 +class _SimpleDemo extends StatelessWidget { + const _SimpleDemo(); - return TCellGroup( - cells: [ - TCell( - title: '单个选择日历', - arrow: true, - note: singleSelected.isEmpty - ? '--' - : () { - final d = DateTime.fromMillisecondsSinceEpoch(singleSelected.first); - return '${d.year}-${d.month}-${d.day}'; - }(), - onClick: (cell) { - // 打开时:若已有选中值则默认展开天气,否则收起 - singleWeatherExpanded.value = singleSelected.isNotEmpty; - TCalendarPopup( - context, - visible: true, - onConfirm: (value) { - singleSelected = value; - refreshTrigger.value++; - }, - onClose: () {}, - child: TCalendar( - title: '请选择日期', - value: singleSelected, - height: size.height * 0.6 + 176, - bottomExpandedListenable: singleWeatherExpanded, - onCellClick: (value, type, tdate) { - // 点击日期时展开天气面板 - singleWeatherExpanded.value = true; - }, - onCellLongPress: (value, type, tdate) { - print('onCellLongPress: $value'); - }, - onHeaderClick: (index, week) { - print('onHeaderClick: $week'); - }, - onChange: (value) { - print('onChange: $value'); - }, - bottom: (context, selectedDates) { - // 随机天气数据 - final weathers = ['☀️ 晴', '⛅ 多云', '🌧️ 小雨', '⛈️ 雷阵雨', '❄️ 小雪', '🌫️ 雾']; - final windDirs = ['北风', '南风', '东风', '西风', '微风']; - final w = weathers[DateTime.now().millisecond % weathers.length]; - final temp = -5 + (DateTime.now().millisecond % 30); - final hum = 30 + (DateTime.now().millisecond % 50); - final wind = windDirs[DateTime.now().millisecond % windDirs.length]; - final windLv = 1 + (DateTime.now().millisecond % 5); - final d = selectedDates.isEmpty - ? DateTime.now() - : DateTime.fromMillisecondsSinceEpoch(selectedDates.first); - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: const [ - BoxShadow( - color: Color.fromRGBO(0, 0, 0, 0.04), - blurRadius: 12, - offset: Offset(0, -2), - ), - ], - ), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('${d.year}-${d.month}-${d.day}', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), - const SizedBox(height: 4), - Text(w, style: const TextStyle(fontSize: 22)), - ], - ), - const SizedBox(width: 24), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [const Icon(Icons.thermostat, size: 14), const SizedBox(width: 4), Text('$temp°C')]), - const SizedBox(height: 4), - Row(children: [const Icon(Icons.water_drop, size: 14), const SizedBox(width: 4), Text('$hum%')]), - const SizedBox(height: 4), - Row(children: [const Icon(Icons.air, size: 14), const SizedBox(width: 4), Text('$wind $windLv 级')]), - ], - ), - ), - ], - ), - ); - }, - ), - ); + @override + Widget build(BuildContext context) { + return const Column( + children: [ + _SingleCalendarCell(), + _MultipleCalendarCell(), + _RangeCalendarCell(), + _SingleTimeCalendarCell(), + _RangeTimeCalendarCell(), + _AnchorCalendarCell(), + ], + ); + } +} + +// ========================= 1. 单选 + 天气 ========================= +class _SingleCalendarCell extends StatefulWidget { + const _SingleCalendarCell(); + @override + State<_SingleCalendarCell> createState() => _SingleCalendarCellState(); +} + +class _SingleCalendarCellState extends State<_SingleCalendarCell> { + List _selected = const []; + final ValueNotifier _expanded = ValueNotifier(false); + final Map _cache = {}; + + @override + void dispose() { + _expanded.dispose(); + super.dispose(); + } + + _WeatherData _weatherFor(DateTime date) { + final key = date.year * 10000 + date.month * 100 + date.day; + return _cache.putIfAbsent(key, () => _WeatherData.random(key)); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + return TCell( + title: '单个选择日历', + arrow: true, + note: _formatYmd(_selected), + onClick: (_) { + _expanded.value = _selected.isNotEmpty; + TCalendarPopup( + context, + visible: true, + onConfirm: (value) => setState(() => _selected = value), + onClose: () => _expanded.value = false, + child: TCalendar( + title: '请选择日期', + value: _selected, + height: size.height * 0.6 + 176, + bottomExpanded: _expanded, + onCellClick: (value, type, tdate) => _expanded.value = true, + bottom: (ctx, dates) { + final d = dates.isEmpty + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(dates.first); + return _WeatherPanel(date: d, weather: _weatherFor(d)); }, ), - TCell( - title: '多个选择日历', - arrow: true, - note: multipleNote, - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - onConfirm: (value) { - multipleDates = value; - refreshTrigger.value++; - }, - child: TCalendar( - title: '请选择日期', - type: CalendarType.multiple, - value: multipleDates.isEmpty - ? [DateTime.now().millisecondsSinceEpoch] - : multipleDates, - height: size.height * 0.6 + 176, - bottom: (context, selectedDates) { - final dates = selectedDates - .map(DateTime.fromMillisecondsSinceEpoch) - .toList() - ..sort(); - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: const [ - BoxShadow( - color: Color.fromRGBO(0, 0, 0, 0.04), - blurRadius: 12, - offset: Offset(0, -2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '已选择 ${dates.length} 天', - style: const TextStyle( - fontSize: 14, fontWeight: FontWeight.w500), - ), - const SizedBox(height: 6), - Wrap( - spacing: 8, - runSpacing: 6, - children: dates - .map((d) => Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: TTheme.of(context).brandColor1, - borderRadius: - BorderRadius.circular(4), - ), - child: Text( - '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}', - style: TextStyle( - fontSize: 12, - color: TTheme.of(context) - .brandColor7), - ), - )) - .toList(), - ), - ], - ), - ); - }, - ), - ); - }, + ); + }, + ); + } +} + +// ========================= 2. 多选 ========================= +class _MultipleCalendarCell extends StatefulWidget { + const _MultipleCalendarCell(); + @override + State<_MultipleCalendarCell> createState() => _MultipleCalendarCellState(); +} + +class _MultipleCalendarCellState extends State<_MultipleCalendarCell> { + List _dates = const []; + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + return TCell( + title: '多个选择日历', + arrow: true, + note: _dates.isEmpty ? '--' : '已选 ${_dates.length} 天', + onClick: (_) { + TCalendarPopup( + context, + visible: true, + onConfirm: (value) => setState(() => _dates = value), + child: TCalendar( + title: '请选择日期', + type: CalendarType.multiple, + value: _dates.isEmpty + ? [DateTime.now().millisecondsSinceEpoch] + : _dates, + height: size.height * 0.6 + 176, + bottom: (ctx, dates) => _MultipleSummary(selected: dates), ), - TCell( - title: '区间选择日历', - arrow: true, - note: rangeDates.length >= 2 - ? '${fmtRange(rangeDates.first)} ~ ${fmtRange(rangeDates[1])}' - : '--', - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - onConfirm: (value) { - rangeDates = value; - refreshTrigger.value++; - }, - child: TCalendar( - title: '请选择日期区间', - type: CalendarType.range, - value: rangeDates, - height: size.height * 0.6 + 176, - bottom: (context, selectedDates) { - String formatDate(int ms) { - final d = DateTime.fromMillisecondsSinceEpoch(ms); - return '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; - } + ); + }, + ); + } +} - final hasStart = selectedDates.isNotEmpty; - final hasEnd = selectedDates.length >= 2; - final days = hasEnd - ? ((selectedDates[1] - selectedDates[0]) / - (24 * 60 * 60 * 1000)) - .round() + - 1 - : (hasStart ? 1 : 0); +// ========================= 3. 区间 ========================= +class _RangeCalendarCell extends StatefulWidget { + const _RangeCalendarCell(); + @override + State<_RangeCalendarCell> createState() => _RangeCalendarCellState(); +} - Widget buildSegment(String label, String? value) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - fontSize: 12, - color: TTheme.of(context).fontGyColor3), - ), - const SizedBox(height: 2), - Text( - value ?? '--', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: value != null - ? TTheme.of(context).fontGyColor1 - : TTheme.of(context).fontGyColor3, - ), - ), - ], - ); - } +class _RangeCalendarCellState extends State<_RangeCalendarCell> { + late List _dates = [ + DateTime.now().millisecondsSinceEpoch, + DateTime.now().add(const Duration(days: 6)).millisecondsSinceEpoch, + ]; - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: const [ - BoxShadow( - color: Color.fromRGBO(0, 0, 0, 0.04), - blurRadius: 12, - offset: Offset(0, -2), - ), - ], - ), - child: Row( - children: [ - Expanded( - child: buildSegment('开始', - hasStart ? formatDate(selectedDates[0]) : null), - ), - Icon(Icons.arrow_forward, - size: 16, - color: TTheme.of(context).fontGyColor3), - const SizedBox(width: 12), - Expanded( - child: buildSegment('结束', - hasEnd ? formatDate(selectedDates[1]) : null), - ), - if (days > 0) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: TTheme.of(context).brandColor1, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - '共 $days 天', - style: TextStyle( - fontSize: 12, - color: TTheme.of(context).brandColor7), - ), - ), - ], - ), - ); - }, - ), - ); - }, + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + return TCell( + title: '区间选择日历', + arrow: true, + note: _dates.length >= 2 + ? '${_formatMd(_dates.first)} ~ ${_formatMd(_dates[1])}' + : '--', + onClick: (_) { + TCalendarPopup( + context, + visible: true, + onConfirm: (value) => setState(() => _dates = value), + child: TCalendar( + title: '请选择日期区间', + type: CalendarType.range, + value: _dates, + height: size.height * 0.6 + 176, + bottom: (ctx, dates) => _RangeSummary(selected: dates), ), - TCell( - title: '单个选择日历和时间', - arrow: true, - note: - '${date.year}-${date.month}-${date.day} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}', - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - onConfirm: (dates) { - // 将 Picker 选中的时分合并到日期时间戳 - final merged = dates.map((ms) { - final d = DateTime.fromMillisecondsSinceEpoch(ms); - return DateTime( - d.year, - d.month, - d.day, - pickedTime.value[0], - pickedTime.value[1], - ).millisecondsSinceEpoch; - }).toList(); - print('onConfirm:$merged'); - selected.value = merged; - refreshTrigger.value++; - }, - onClose: () { - print('onClose'); - }, - child: TCalendar( - title: '请选择日期和时间', - value: selected.value, - height: size.height * 0.92, - bottom: (context, selectedDates) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: const [ - BoxShadow( - color: Color.fromRGBO(0, 0, 0, 0.04), - blurRadius: 12, - offset: Offset(0, -2), - ), - ], - ), - child: TPicker( - items: timeItems, - initialValue: pickedTime.value, - height: 180, - itemCount: 5, - title: '选择时间', - onChange: (v) => - pickedTime.value = List.from(v.values), - ), - ); - }, - onCellClick: (value, type, tdate) { - print('onCellClick: $value'); - }, - onCellLongPress: (value, type, tdate) { - print('onCellLongPress: $value'); - }, - onHeaderClick: (index, week) { - print('onHeaderClick: $week'); - }, - onChange: (value) { - print('onChange: $value'); - }, - ), - ); - }, + ); + }, + ); + } +} + +// ========================= 4. 单选 + 时间 ========================= +class _SingleTimeCalendarCell extends StatefulWidget { + const _SingleTimeCalendarCell(); + @override + State<_SingleTimeCalendarCell> createState() => + _SingleTimeCalendarCellState(); +} + +class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> { + late List _selected = [ + DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000, + ]; + late final ValueNotifier> _pickedTime; + late final TPickerColumns _timeItems = _buildTimeItems(); + + @override + void initState() { + super.initState(); + final now = DateTime.now(); + _pickedTime = ValueNotifier>([now.hour, now.minute]); + } + + @override + void dispose() { + _pickedTime.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final d = DateTime.fromMillisecondsSinceEpoch(_selected.first); + return TCell( + title: '单个选择日历和时间', + arrow: true, + note: '${d.year}-${d.month}-${d.day} ' + '${d.hour.toString().padLeft(2, '0')}:' + '${d.minute.toString().padLeft(2, '0')}', + onClick: (_) { + TCalendarPopup( + context, + visible: true, + onConfirm: (dates) { + final merged = dates.map((ms) { + return DateTime.fromMillisecondsSinceEpoch(ms) + .copyWith( + hour: _pickedTime.value[0], + minute: _pickedTime.value[1], + ) + .millisecondsSinceEpoch; + }).toList(); + setState(() => _selected = merged); + }, + child: TCalendar( + title: '请选择日期和时间', + value: _selected, + height: size.height * 0.92, + bottom: (ctx, _) => _TimePickerPanel( + items: _timeItems, + initialValue: _pickedTime.value, + title: '选择时间', + onChange: (v) => _pickedTime.value = v, + ), ), - TCell( - title: '区间选择日历和时间', - arrow: true, - note: rangeTimeNote, - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - onConfirm: (value) { - // 把开始/结束时分合并到对应日期 - final rt = pickedRangeTime.value; - final merged = [ - for (var i = 0; i < value.length; i++) - DateTime.fromMillisecondsSinceEpoch(value[i]) - .copyWith(hour: rt[i][0], minute: rt[i][1]) - .millisecondsSinceEpoch, - ]; - print('onConfirm: $merged'); - rangeTimeDates = merged; - refreshTrigger.value++; - }, - onClose: () { - print('onClose'); - }, - child: TCalendar( - title: '请选择日期和时间区间', - height: size.height * 0.92, - type: CalendarType.range, - value: [ + ); + }, + ); + } +} + +// ========================= 5. 区间 + 时间 ========================= +class _RangeTimeCalendarCell extends StatefulWidget { + const _RangeTimeCalendarCell(); + @override + State<_RangeTimeCalendarCell> createState() => _RangeTimeCalendarCellState(); +} + +class _RangeTimeCalendarCellState extends State<_RangeTimeCalendarCell> { + List _dates = const []; + late List> _pickedRangeTime; + int _currentTab = 0; + late final TPickerColumns _timeItems = _buildTimeItems(); + + @override + void initState() { + super.initState(); + final now = DateTime.now(); + _pickedRangeTime = [ + [now.hour, now.minute], + [now.hour, now.minute], + ]; + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + return TCell( + title: '区间选择日历和时间', + arrow: true, + note: _dates.length >= 2 + ? '${_formatMdHm(_dates.first)} ~ ${_formatMdHm(_dates.last)}' + : '--', + onClick: (_) { + TCalendarPopup( + context, + visible: true, + onConfirm: (value) { + final merged = [ + for (var i = 0; i < value.length; i++) + DateTime.fromMillisecondsSinceEpoch(value[i]) + .copyWith( + hour: _pickedRangeTime[i][0], + minute: _pickedRangeTime[i][1], + ) + .millisecondsSinceEpoch, + ]; + setState(() => _dates = merged); + }, + child: TCalendar( + title: '请选择日期和时间区间', + height: size.height * 0.92, + type: CalendarType.range, + value: _dates.isEmpty + ? [ DateTime.now().millisecondsSinceEpoch, DateTime.now() .add(const Duration(days: 3)) .millisecondsSinceEpoch, - ], - bottom: (context, selectedDates) { - return DefaultTabController( - length: 2, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: const [ - BoxShadow( - color: Color.fromRGBO(0, 0, 0, 0.04), - blurRadius: 12, - offset: Offset(0, -2), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TTabBar( - height: 40, - showIndicator: true, - tabs: const [ - TTab(text: '开始时间'), - TTab(text: '结束时间'), - ], - onTap: (i) => rangeTimeTab.value = i, - ), - ValueListenableBuilder( - valueListenable: rangeTimeTab, - builder: (context, tab, _) => TPicker( - // 切换 tab 时重建,复位到对应时分 - key: ValueKey(tab), - items: timeItems, - initialValue: pickedRangeTime.value[tab], - height: 180, - itemCount: 5, - onChange: (v) { - final next = [...pickedRangeTime.value]; - next[tab] = List.from(v.values); - pickedRangeTime.value = next; - }, - ), - ), - ], - ), + ] + : _dates, + bottom: (ctx, _) => _RangeTimePickerPanel( + items: _timeItems, + currentTab: _currentTab, + initialValues: _pickedRangeTime, + onTabChanged: (tab) => _currentTab = tab, + onPickerChanged: (tab, value) { + _pickedRangeTime[tab] = value; + }, + ), + ), + ); + }, + ); + } +} + +// ========================= 6. 锚点 ========================= +class _AnchorCalendarCell extends StatefulWidget { + const _AnchorCalendarCell(); + @override + State<_AnchorCalendarCell> createState() => _AnchorCalendarCellState(); +} + +class _AnchorCalendarCellState extends State<_AnchorCalendarCell> { + late List _selected = [ + DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000, + ]; + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + return TCell( + title: '添加锚点', + arrow: true, + note: _formatYmd(_selected), + onClick: (_) { + TCalendarPopup( + context, + visible: true, + onConfirm: (dates) => setState(() => _selected = dates), + child: TCalendar( + title: '请选择日期', + minDate: DateTime(2022, 1, 1).millisecondsSinceEpoch, + maxDate: DateTime(2028, 2, 15).millisecondsSinceEpoch, + anchorDate: DateTime(2026, 5), + value: _selected, + height: size.height * 0.6 + 176, + ), + ); + }, + ); + } +} + +// ===== 共用:构建时间选择器 items ===== +TPickerColumns _buildTimeItems() => TPickerColumns([ + [ + for (int i = 0; i < 24; i++) + TPickerOption(label: '${i.toString().padLeft(2, '0')}时', value: i), + ], + [ + for (int i = 0; i < 60; i++) + TPickerOption(label: '${i.toString().padLeft(2, '0')}分', value: i), + ], + ]); + +// ===== 顶层格式化辅助函数 ===== +String _formatYmd(List dates) { + if (dates.isEmpty) { + return '--'; + } + final d = DateTime.fromMillisecondsSinceEpoch(dates.first); + return '${d.year}-${d.month}-${d.day}'; +} + +String _formatMd(int ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return '${d.month}/${d.day}'; +} + +String _formatMdHm(int ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return '${d.month}/${d.day} ' + '${d.hour.toString().padLeft(2, '0')}:' + '${d.minute.toString().padLeft(2, '0')}'; +} + +String _formatYmdFull(int ms) { + final d = DateTime.fromMillisecondsSinceEpoch(ms); + return '${d.year}-${d.month.toString().padLeft(2, '0')}-' + '${d.day.toString().padLeft(2, '0')}'; +} + +// ===== 共用 bottom 面板装饰 ===== +BoxDecoration _bottomCardDecoration(BuildContext context) => BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.04), + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ); + +// ===== 天气数据模型 ===== +class _WeatherData { + const _WeatherData({ + required this.icon, + required this.temp, + required this.humidity, + required this.wind, + required this.windLevel, + }); + + final String icon; + final int temp; + final int humidity; + final String wind; + final int windLevel; + + factory _WeatherData.random(int seed) { + const weathers = ['☀️ 晴', '⛅ 多云', '🌧️ 小雨', '⛈️ 雷阵雨', '❄️ 小雪', '🌫️ 雾']; + const winds = ['北风', '南风', '东风', '西风', '微风']; + return _WeatherData( + icon: weathers[seed % weathers.length], + temp: -5 + (seed % 30), + humidity: 30 + (seed % 50), + wind: winds[seed % winds.length], + windLevel: 1 + (seed % 5), + ); + } +} + +// ===== 拆分出的私有 widget ===== + +class _WeatherPanel extends StatelessWidget { + const _WeatherPanel({required this.date, required this.weather}); + + final DateTime date; + final _WeatherData weather; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: _bottomCardDecoration(context), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${date.year}-${date.month}-${date.day}', + style: const TextStyle( + fontSize: 14, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 4), + Text(weather.icon, style: const TextStyle(fontSize: 22)), + ], + ), + const SizedBox(width: 24), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _IconRow(icon: Icons.thermostat, text: '${weather.temp}°C'), + const SizedBox(height: 4), + _IconRow(icon: Icons.water_drop, text: '${weather.humidity}%'), + const SizedBox(height: 4), + _IconRow( + icon: Icons.air, + text: '${weather.wind} ${weather.windLevel} 级'), + ], + ), + ), + ], + ), + ); + } +} + +class _IconRow extends StatelessWidget { + const _IconRow({required this.icon, required this.text}); + + final IconData icon; + final String text; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Icon(icon, size: 14), + const SizedBox(width: 4), + Text(text), + ], + ); + } +} + +class _MultipleSummary extends StatelessWidget { + const _MultipleSummary({required this.selected}); + + final List selected; + + @override + Widget build(BuildContext context) { + final dates = [...selected]..sort(); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: _bottomCardDecoration(context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text('已选择 ${dates.length} 天', + style: + const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + const SizedBox(height: 6), + Wrap( + spacing: 8, + runSpacing: 6, + children: dates + .map((ms) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: TTheme.of(context).brandColor1, + borderRadius: BorderRadius.circular(4), ), - ); - }, - onChange: (value) { - print('onChange: $value'); - }, - ), - ); - }, + child: Text( + _formatYmdFull(ms), + style: TextStyle( + fontSize: 12, + color: TTheme.of(context).brandColor7), + ), + )) + .toList(), ), - TCell( - title: '添加锚点', - arrow: true, - note: '${date.year}-${date.month}-${date.day}', - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - onConfirm: (dates) { - print('onConfirm:$dates'); - selected.value = dates; - refreshTrigger.value++; - }, - onClose: () { - print('onClose'); - }, - child: TCalendar( - title: '请选择日期', - minDate: DateTime(2022, 1, 1).millisecondsSinceEpoch, - maxDate: DateTime(2028, 2, 15).millisecondsSinceEpoch, - anchorDate: DateTime(2026, 5), - value: selected.value, - height: size.height * 0.6 + 176, - onCellClick: (value, type, tdate) { - print('onCellClick: $value'); - }, - onCellLongPress: (value, type, tdate) { - print('onCellLongPress: $value'); - }, - onHeaderClick: (index, week) { - print('onHeaderClick: $week'); - }, - onChange: (value) { - print('onChange: $value'); - }, - ), - ); - }, + ], + ), + ); + } +} + +class _RangeSummary extends StatelessWidget { + const _RangeSummary({required this.selected}); + + final List selected; + + @override + Widget build(BuildContext context) { + final hasStart = selected.isNotEmpty; + final hasEnd = selected.length >= 2; + final days = hasEnd + ? ((selected[1] - selected[0]) / (24 * 60 * 60 * 1000)).round() + 1 + : (hasStart ? 1 : 0); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: _bottomCardDecoration(context), + child: Row( + children: [ + Expanded( + child: _RangeSegment( + label: '开始', + value: hasStart ? _formatYmdFull(selected[0]) : null), + ), + Icon(Icons.arrow_forward, + size: 16, color: TTheme.of(context).fontGyColor3), + const SizedBox(width: 12), + Expanded( + child: _RangeSegment( + label: '结束', + value: hasEnd ? _formatYmdFull(selected[1]) : null), ), + if (days > 0) + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: TTheme.of(context).brandColor1, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '共 $days 天', + style: TextStyle( + fontSize: 12, color: TTheme.of(context).brandColor7), + ), + ), ], - ); - }, - ); + ), + ); + } +} + +class _RangeSegment extends StatelessWidget { + const _RangeSegment({required this.label, required this.value}); + + final String label; + final String? value; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: TextStyle( + fontSize: 12, color: TTheme.of(context).fontGyColor3)), + const SizedBox(height: 2), + Text( + value ?? '--', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: value != null + ? TTheme.of(context).fontGyColor1 + : TTheme.of(context).fontGyColor3, + ), + ), + ], + ); + } +} + +class _TimePickerPanel extends StatelessWidget { + const _TimePickerPanel({ + required this.items, + required this.initialValue, + required this.title, + required this.onChange, + }); + + final TPickerColumns items; + final List initialValue; + final String title; + final ValueChanged> onChange; + + @override + Widget build(BuildContext context) { + return Container( + decoration: _bottomCardDecoration(context), + child: TPicker( + items: items, + initialValue: initialValue, + height: 180, + itemCount: 5, + title: title, + onChange: (v) => onChange(List.from(v.values)), + ), + ); + } +} + +/// 区间+时间 demo 的双时间选择器面板(Tab 切换开始/结束) +/// 使用回调模式:子组件不持有 ValueNotifier,数据由父管理。 +class _RangeTimePickerPanel extends StatefulWidget { + const _RangeTimePickerPanel({ + required this.items, + required this.currentTab, + required this.initialValues, + required this.onTabChanged, + required this.onPickerChanged, + }); + + final TPickerColumns items; + final int currentTab; + final List> initialValues; + final ValueChanged onTabChanged; + final void Function(int tab, List value) onPickerChanged; + + @override + State<_RangeTimePickerPanel> createState() => _RangeTimePickerPanelState(); +} + +class _RangeTimePickerPanelState extends State<_RangeTimePickerPanel> { + late int _tab = widget.currentTab; + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + child: Container( + decoration: _bottomCardDecoration(context), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TTabBar( + height: 40, + showIndicator: true, + tabs: const [ + TTab(text: '开始时间'), + TTab(text: '结束时间'), + ], + onTap: (i) { + setState(() => _tab = i); + widget.onTabChanged(i); + }, + ), + TPicker( + key: ValueKey(_tab), + items: widget.items, + initialValue: widget.initialValues[_tab], + height: 180, + itemCount: 5, + onChange: (v) => + widget.onPickerChanged(_tab, List.from(v.values)), + ), + ], + ), + ), + ); + } } @Demo(group: 'calendar') diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart index c2d978af0..b12a30b79 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart @@ -6,10 +6,10 @@ import '../../util/iterable_ext.dart'; export 't_calendar_body.dart'; export 't_calendar_cell.dart'; +export 't_calendar_data_source.dart'; export 't_calendar_header.dart'; export 't_calendar_popup.dart'; export 't_calendar_style.dart'; -export 't_calendar_data_source.dart'; export 't_lunar_date.dart'; typedef CalendarFormat = TDate? Function(TDate? day); @@ -51,8 +51,7 @@ class TCalendar extends StatefulWidget { this.onHeaderClick, this.useSafeArea = true, this.bottom, - this.bottomExpanded = true, - this.bottomExpandedListenable, + this.bottomExpanded, this.monthTitleHeight = 22, this.monthTitleBuilder, this.animateTo = false, @@ -135,15 +134,29 @@ class TCalendar extends StatefulWidget { /// 是否使用安全区域,默认true final bool? useSafeArea; - /// 底部自定义区域构建器,位于日历主体浮层上方 + /// 底部自定义区域构建器,位于日历主体浮层上方。 + /// + /// `selectedDates` 是当前选中日期的毫秒时间戳列表,会随用户点击单元格自动更新。 + /// + /// 典型用法:在 bottom 中渲染时间选择器、统计信息、操作按钮等。 final CalendarBottomBuilder? bottom; - /// bottom 区域是否展开,默认 true - final bool bottomExpanded; - - /// bottom 区域是否展开(响应式版本,优先级高于 [bottomExpanded])。 - /// 传入后,bottom 展开/收起会跟随该 listenable 变化自动播放动画。 - final ValueListenable? bottomExpandedListenable; + /// bottom 区域是否展开(响应式)。 + /// + /// - 传 `null`(默认):bottom 始终展开 + /// - 传 `ValueListenable`:bottom 展开/收起会跟随该 listenable 变化播放滑动动画, + /// 常配合 [ValueNotifier] 实现"按钮触发展开/收起"。 + /// + /// 示例: + /// ```dart + /// final expanded = ValueNotifier(false); + /// TCalendar( + /// bottomExpanded: expanded, + /// onCellClick: (v, t, d) => expanded.value = true, + /// bottom: (ctx, dates) => MyFooter(), + /// ); + /// ``` + final ValueListenable? bottomExpanded; /// 月标题高度 final double? monthTitleHeight; @@ -188,12 +201,17 @@ class _TCalendarState extends State { late TCalendarInherited? inherited; late TCalendarStyle _style; - /// bottom 展开时日历主体固定上移高度 - static const double _bottomOffset = 30.0; + /// bottom 展开时日历主体上移高度,露出 bottom "把手"区域 + /// (设计稿固定值,与 [_buildBottom] 的 bottom offset 配合) + static const double _bottomPeekHeight = 30.0; + + /// 标记是否已完成首次 selected 同步,避免 didChangeDependencies 重复触发 + bool _initializedSelected = false; @override void didChangeDependencies() { super.didChangeDependencies(); + inherited = TCalendarInherited.of(context); weekdayNames = [ context.resource.sunday, context.resource.monday, @@ -218,30 +236,39 @@ class _TCalendarState extends State { context.resource.december, ]; _style = widget.style ?? TCalendarStyle.generateStyle(context); + // 仅首次挂载时同步 widget.value → inherited.selected + if (!_initializedSelected) { + _initializedSelected = true; + _syncSelectedToInherited(); + } } @override - Widget build(BuildContext context) { - inherited = TCalendarInherited.of(context); - _initValue(); - final verticalGap = _style.verticalGap ?? TTheme.of(context).spacer8; - final hasBottom = widget.bottom != null; + void didUpdateWidget(covariant TCalendar oldWidget) { + super.didUpdateWidget(oldWidget); + // 仅当 widget.value 内容变化时,才重置 inherited.selected + if (!listEquals(oldWidget.value, widget.value)) { + _syncSelectedToInherited(); + } + } - if (widget.bottomExpandedListenable != null) { - // 响应式:监听 listenable 变化,让 padding 与 bottom 区动画同步重建 - return ValueListenableBuilder( - valueListenable: widget.bottomExpandedListenable!, - builder: (context, expanded, _) { - return _buildBody(context, verticalGap, hasBottom, expanded); - }, - ); + /// 把 widget.value 同步到 inherited.selected(仅在首次或 value 变化时调用) + void _syncSelectedToInherited() { + if (inherited == null) { + return; } - return _buildBody(context, verticalGap, hasBottom, widget.bottomExpanded); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + inherited!.selected.value = _getValue(widget.value ?? []); + }); } - Widget _buildBody( - BuildContext context, double verticalGap, bool hasBottom, bool expanded) { - final bottomPadding = hasBottom && expanded ? _bottomOffset : 0.0; + @override + Widget build(BuildContext context) { + final verticalGap = _style.verticalGap ?? TTheme.of(context).spacer8; + final hasBottom = widget.bottom != null; return Container( height: widget.height, @@ -269,54 +296,7 @@ class _TCalendarState extends State { onClick: widget.onHeaderClick, ), Expanded( - child: AnimatedPadding( - duration: const Duration(milliseconds: 200), - padding: EdgeInsets.only(bottom: bottomPadding), - child: TCalendarBody( - type: widget.type ?? CalendarType.single, - firstDayOfWeek: widget.firstDayOfWeek ?? 0, - maxDate: widget.maxDate, - anchorDate: widget.anchorDate, - minDate: widget.minDate, - value: widget._value, - bodyPadding: - _style.bodyPadding ?? TTheme.of(context).spacer16, - displayFormat: widget.displayFormat ?? 'year month', - monthNames: monthNames, - monthTitleStyle: _style.monthTitleStyle, - verticalGap: verticalGap, - cellHeight: _getEffectiveCellHeight(), - monthTitleHeight: widget.monthTitleHeight ?? 22, - monthTitleBuilder: widget.monthTitleBuilder, - animateTo: widget.animateTo ?? false, - onMonthChange: widget.onMonthChange, - dateType: widget.dateType, - dataSource: widget.dataSource, - builder: (date, dateList, data, rowIndex, colIndex) { - return TCalendarCell( - height: _getEffectiveCellHeight(), - 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, - cellWidget: widget.cellWidget, - dateType: widget.dateType, - showLunarInfo: widget.showLunarInfo, - ); - }, - ), - ), + child: _buildAnimatedBody(verticalGap, hasBottom), ), if (inherited?.usePopup == true) inherited?.confirmBtn ?? @@ -337,13 +317,85 @@ class _TCalendarState extends State { SizedBox(height: MediaQuery.of(context).padding.bottom) ], ), - if (hasBottom) _buildBottom(expanded), + if (hasBottom) _buildBottom(), ], ), ); } - Widget _buildBottom(bool expanded) { + /// 仅在此处监听 bottomExpanded,局部化重建范围(只影响 padding 动画) + Widget _buildAnimatedBody(double verticalGap, bool hasBottom) { + if (!hasBottom || widget.bottomExpanded == null) { + // 无 bottom 或始终展开 → 静态 padding + final padding = hasBottom ? _bottomPeekHeight : 0.0; + return Padding( + padding: EdgeInsets.only(bottom: padding), + child: _buildCalendarBody(verticalGap), + ); + } + // 响应式 → 仅 AnimatedPadding 重建 + return ValueListenableBuilder( + valueListenable: widget.bottomExpanded!, + builder: (context, expanded, child) { + return AnimatedPadding( + duration: const Duration(milliseconds: 200), + padding: EdgeInsets.only( + bottom: expanded ? _bottomPeekHeight : 0.0), + child: child!, + ); + }, + child: _buildCalendarBody(verticalGap), + ); + } + + Widget _buildCalendarBody(double verticalGap) { + return TCalendarBody( + type: widget.type ?? CalendarType.single, + firstDayOfWeek: widget.firstDayOfWeek ?? 0, + maxDate: widget.maxDate, + anchorDate: widget.anchorDate, + minDate: widget.minDate, + value: widget._value, + bodyPadding: _style.bodyPadding ?? TTheme.of(context).spacer16, + displayFormat: widget.displayFormat ?? 'year month', + monthNames: monthNames, + monthTitleStyle: _style.monthTitleStyle, + verticalGap: verticalGap, + cellHeight: _getEffectiveCellHeight(), + monthTitleHeight: widget.monthTitleHeight ?? 22, + monthTitleBuilder: widget.monthTitleBuilder, + animateTo: widget.animateTo ?? false, + onMonthChange: widget.onMonthChange, + dateType: widget.dateType, + dataSource: widget.dataSource, + builder: (date, dateList, data, rowIndex, colIndex) { + return TCalendarCell( + height: _getEffectiveCellHeight(), + 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, + cellWidget: widget.cellWidget, + dateType: widget.dateType, + showLunarInfo: widget.showLunarInfo, + ); + }, + ); + } + + /// bottom 浮层:仅 AnimatedSlide 响应 expanded,其余内容通过 inherited.selected 驱动 + Widget _buildBottom() { final bottomOffset = (inherited?.usePopup == true) ? (widget.useSafeArea == true ? MediaQuery.of(context).padding.bottom + @@ -354,24 +406,44 @@ class _TCalendarState extends State { ? MediaQuery.of(context).padding.bottom : 0.0); + // bottom 内容:跟随选中日期更新 + final content = inherited != null + ? ValueListenableBuilder>( + valueListenable: inherited!.selected, + builder: (context, selectedDates, _) { + return widget.bottom!(context, selectedDates); + }, + ) + : widget.bottom!(context, widget.value ?? []); + + // 如果有 bottomExpanded listenable → 用 ValueListenableBuilder 局部化 slide 动画 + if (widget.bottomExpanded != null) { + return Positioned( + left: 0, + right: 0, + bottom: bottomOffset, + child: ClipRect( + child: ValueListenableBuilder( + valueListenable: widget.bottomExpanded!, + builder: (context, expanded, child) { + return AnimatedSlide( + duration: const Duration(milliseconds: 200), + offset: expanded ? Offset.zero : const Offset(0, 1), + child: child!, + ); + }, + child: content, + ), + ), + ); + } + + // 无 listenable → 始终展开 return Positioned( left: 0, right: 0, bottom: bottomOffset, - child: ClipRect( - child: AnimatedSlide( - duration: const Duration(milliseconds: 200), - offset: expanded ? Offset.zero : const Offset(0, 1), - child: inherited != null - ? ValueListenableBuilder>( - valueListenable: inherited!.selected, - builder: (context, selectedDates, _) { - return widget.bottom!(context, selectedDates); - }, - ) - : widget.bottom!(context, widget.value ?? []), - ), - ), + child: content, ); } @@ -382,14 +454,6 @@ class _TCalendarState extends State { }).toList(); } - void _initValue() { - if (inherited == null) { - return; - } - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - inherited!.selected.value = _getValue(widget.value ?? []); - }); - } /// 获取有效的单元格高度 /// 当显示农历信息时,需要更大的高度以容纳额外的文本 double _getEffectiveCellHeight() { From 3450574ac9a0bfd77adf8ef77e57aa5d3f5bd050 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 08:00:28 +0000 Subject: [PATCH 04/35] [autofix.ci] apply automated fixes --- .../example/assets/api/action-sheet_api.md | 38 +- .../example/assets/api/button_api.md | 50 +- .../example/assets/api/calendar_api.md | 68 +- .../example/assets/api/cell_api.md | 78 +-- .../example/assets/api/checkbox_api.md | 44 +- .../example/assets/api/dialog_api.md | 74 +-- .../example/assets/api/drawer_api.md | 54 +- .../example/assets/api/dropdown-menu_api.md | 51 +- .../example/assets/api/form_api.md | 58 +- .../example/assets/api/image-viewer_api.md | 22 +- .../example/assets/api/indexes_api.md | 48 +- .../example/assets/api/message_api.md | 48 +- .../example/assets/api/notice-bar_api.md | 48 +- .../example/assets/api/picker_api.md | 68 +- .../example/assets/api/popover_api.md | 26 +- .../example/assets/api/popup_api.md | 52 +- .../example/assets/api/side-bar_api.md | 32 +- .../example/assets/api/skeleton_api.md | 40 +- .../example/assets/api/steps_api.md | 30 +- .../example/assets/api/swiper_api.md | 30 +- .../example/assets/api/tab-bar_api.md | 64 +- .../example/assets/api/tabs_api.md | 26 +- .../example/assets/api/time-counter_api.md | 50 +- .../example/assets/api/tree-select_api.md | 30 +- tdesign-site/src/action-sheet/README.md | 38 +- tdesign-site/src/button/README.md | 50 +- tdesign-site/src/calendar/README.md | 610 +----------------- tdesign-site/src/cell/README.md | 78 +-- tdesign-site/src/checkbox/README.md | 44 +- tdesign-site/src/dialog/README.md | 74 +-- tdesign-site/src/drawer/README.md | 54 +- tdesign-site/src/dropdown-menu/README.md | 52 +- tdesign-site/src/form/README.md | 58 +- tdesign-site/src/image-viewer/README.md | 22 +- tdesign-site/src/indexes/README.md | 48 +- tdesign-site/src/message/README.md | 48 +- tdesign-site/src/notice-bar/README.md | 48 +- tdesign-site/src/picker/README.md | 68 +- tdesign-site/src/popover/README.md | 26 +- tdesign-site/src/popup/README.md | 52 +- tdesign-site/src/side-bar/README.md | 32 +- tdesign-site/src/steps/README.md | 30 +- tdesign-site/src/swiper/README.md | 30 +- tdesign-site/src/tab-bar/README.md | 64 +- tdesign-site/src/tabs/README.md | 26 +- tdesign-site/src/time-counter/README.md | 50 +- tdesign-site/src/tree-select/README.md | 30 +- 47 files changed, 1111 insertions(+), 1650 deletions(-) diff --git a/tdesign-component/example/assets/api/action-sheet_api.md b/tdesign-component/example/assets/api/action-sheet_api.md index 8c7efa525..a08aa51e4 100644 --- a/tdesign-component/example/assets/api/action-sheet_api.md +++ b/tdesign-component/example/assets/api/action-sheet_api.md @@ -1,4 +1,23 @@ ## API +### TActionSheetItem +#### 简介 +动作面板项目 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| badge | TBadge? | - | 角标 | +| description | String? | - | 描述信息 | +| disabled | bool | false | 是否禁用 | +| group | String? | - | 分组,用于带描述多行滚动宫格 | +| icon | Widget? | - | 图标 | +| iconSize | double? | - | 图标大小 | +| label | String | - | 标题 | +| textStyle | TextStyle? | - | 标题样式 | + +``` +``` + ### TActionSheet #### 简介 动作面板 @@ -35,22 +54,3 @@ | showGridActionSheet | | required BuildContext context, required List items, TActionSheetAlign align, String? cancelText, bool showCancel, TActionSheetItemCallback? onSelected, bool showOverlay, bool closeOnOverlayClick, int count, int rows, double itemHeight, double itemMinWidth, bool scrollable, bool showPagination, VoidCallback? onCancel, String? description, VoidCallback? onClose, bool useSafeArea, | 显示宫格类型面板 | | showGroupActionSheet | | required BuildContext context, required List items, TActionSheetAlign align, String? cancelText, bool showCancel, TActionSheetItemCallback? onSelected, bool showOverlay, bool closeOnOverlayClick, double itemHeight, double itemMinWidth, VoidCallback? onCancel, VoidCallback? onClose, bool useSafeArea, | 显示分组类型面板 | | showListActionSheet | | required BuildContext context, required List items, TActionSheetAlign align, String? cancelText, bool showCancel, VoidCallback? onCancel, TActionSheetItemCallback? onSelected, bool showOverlay, bool closeOnOverlayClick, VoidCallback? onClose, bool useSafeArea, | 显示列表类型面板 | - -``` -``` - -### TActionSheetItem -#### 简介 -动作面板项目 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| badge | TBadge? | - | 角标 | -| description | String? | - | 描述信息 | -| disabled | bool | false | 是否禁用 | -| group | String? | - | 分组,用于带描述多行滚动宫格 | -| icon | Widget? | - | 图标 | -| iconSize | double? | - | 图标大小 | -| label | String | - | 标题 | -| textStyle | TextStyle? | - | 标题样式 | diff --git a/tdesign-component/example/assets/api/button_api.md b/tdesign-component/example/assets/api/button_api.md index bb540f5c0..be9eb3640 100644 --- a/tdesign-component/example/assets/api/button_api.md +++ b/tdesign-component/example/assets/api/button_api.md @@ -1,29 +1,4 @@ ## API -### TButtonStyle -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| backgroundColor | Color? | - | 背景颜色 | -| frameColor | Color? | - | 边框颜色 | -| frameWidth | double? | - | 边框宽度 | -| gradient | Gradient? | - | 渐变背景色 | -| radius | BorderRadiusGeometry? | - | 自定义圆角 | -| textColor | Color? | - | 文字颜色 | - - -#### 工厂构造方法 - -| 名称 | 说明 | -| --- | --- | -| TButtonStyle.generateFillStyleByTheme | 生成不同主题的填充按钮样式 | -| TButtonStyle.generateGhostStyleByTheme | 生成不同主题的幽灵按钮样式 | -| TButtonStyle.generateOutlineStyleByTheme | 生成不同主题的描边按钮样式 | -| TButtonStyle.generateTextStyleByTheme | 生成不同主题的文本按钮样式 | - -``` -``` - ### TButton #### 默认构造方法 @@ -54,3 +29,28 @@ | theme | TButtonTheme? | - | 主题 | | type | TButtonType | TButtonType.fill | 类型:填充,描边,文字 | | width | double? | - | 自定义宽度 | + +``` +``` + +### TButtonStyle +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| backgroundColor | Color? | - | 背景颜色 | +| frameColor | Color? | - | 边框颜色 | +| frameWidth | double? | - | 边框宽度 | +| gradient | Gradient? | - | 渐变背景色 | +| radius | BorderRadiusGeometry? | - | 自定义圆角 | +| textColor | Color? | - | 文字颜色 | + + +#### 工厂构造方法 + +| 名称 | 说明 | +| --- | --- | +| TButtonStyle.generateFillStyleByTheme | 生成不同主题的填充按钮样式 | +| TButtonStyle.generateGhostStyleByTheme | 生成不同主题的幽灵按钮样式 | +| TButtonStyle.generateOutlineStyleByTheme | 生成不同主题的描边按钮样式 | +| TButtonStyle.generateTextStyleByTheme | 生成不同主题的文本按钮样式 | diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md index e6bfea320..fda9741d4 100644 --- a/tdesign-component/example/assets/api/calendar_api.md +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -1,32 +1,4 @@ ## API -### TCalendarStyle -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| cellDecoration | BoxDecoration? | - | 日期decoration | -| cellPrefixStyle | TextStyle? | - | 日期前面的字符串的样式 | -| cellStyle | TextStyle? | - | 日期样式 | -| cellSuffixStyle | TextStyle? | - | 日期后面的字符串的样式 | -| centreColor | Color? | - | 日期范围内背景样式 | -| decoration | | - | | -| monthTitleStyle | TextStyle? | - | body区域 年月文字样式 | -| titleCloseColor | Color? | - | header区域 关闭图标的颜色 | -| titleMaxLine | int? | - | header区域 [TCalendar.title]的行数 | -| titleStyle | TextStyle? | - | header区域 [TCalendar.title]的样式 | -| weekdayStyle | TextStyle? | - | header区域 周 文字样式 | - - -#### 工厂构造方法 - -| 名称 | 说明 | -| --- | --- | -| TCalendarStyle.cellStyle | 日期样式 | -| TCalendarStyle.generateStyle | 生成默认样式 | - -``` -``` - ### TCalendar #### 默认构造方法 @@ -34,8 +6,8 @@ | --- | --- | --- | --- | | anchorDate | DateTime? | - | 锚点日期 | | animateTo | bool? | false | 动画滚动到指定位置 | -| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,位于日历主体浮层上方 | -| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。传 null 表示始终展开;传 ValueNotifier 时展开/收起会跟随 listenable 变化播放滑动动画 | +| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,位于日历主体浮层上方。 | +| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。 | | cellHeight | double? | 60 | 日期高度 | | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | | dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 | @@ -66,10 +38,6 @@ ``` ``` -### TCalendarDataSource -``` -``` - ### TCalendarPopup #### 默认构造方法 @@ -88,6 +56,38 @@ ``` ``` +### TCalendarStyle +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| cellDecoration | BoxDecoration? | - | 日期decoration | +| cellPrefixStyle | TextStyle? | - | 日期前面的字符串的样式 | +| cellStyle | TextStyle? | - | 日期样式 | +| cellSuffixStyle | TextStyle? | - | 日期后面的字符串的样式 | +| centreColor | Color? | - | 日期范围内背景样式 | +| decoration | | - | | +| monthTitleStyle | TextStyle? | - | body区域 年月文字样式 | +| titleCloseColor | Color? | - | header区域 关闭图标的颜色 | +| titleMaxLine | int? | - | header区域 [TCalendar.title]的行数 | +| titleStyle | TextStyle? | - | header区域 [TCalendar.title]的样式 | +| weekdayStyle | TextStyle? | - | header区域 周 文字样式 | + + +#### 工厂构造方法 + +| 名称 | 说明 | +| --- | --- | +| TCalendarStyle.cellStyle | 日期样式 | +| TCalendarStyle.generateStyle | 生成默认样式 | + +``` +``` + +### TCalendarDataSource +``` +``` + ### TLunarInfo #### 默认构造方法 diff --git a/tdesign-component/example/assets/api/cell_api.md b/tdesign-component/example/assets/api/cell_api.md index a3ed0ea98..b66815628 100644 --- a/tdesign-component/example/assets/api/cell_api.md +++ b/tdesign-component/example/assets/api/cell_api.md @@ -1,4 +1,43 @@ ## API +### TCell +#### 简介 +单元格组件 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| align | TCellAlign? | TCellAlign.middle | 内容的对齐方式,默认居中对齐。可选项:top/middle/bottom | +| arrow | bool? | false | 是否显示右侧箭头 | +| bordered | bool? | true | 是否显示下边框,仅在TCellGroup组件下起作用 | +| description | String? | - | 下方内容描述文字 | +| descriptionWidget | Widget? | - | 下方内容描述组件 | +| disabled | bool? | false | 禁用 | +| height | double? | - | 高度 | +| hover | bool? | true | 是否开启点击反馈 | +| image | ImageProvider? | - | 主图 | +| imageCircle | double? | 50 | 主图圆角,默认50(圆形) | +| imageSize | double? | - | 主图尺寸 | +| imageWidget | Widget? | - | 主图组件 | +| key | | - | | +| leftIcon | IconData? | - | 左侧图标,出现在单元格标题的左侧 | +| leftIconWidget | Widget? | - | 左侧图标组件 | +| note | String? | - | 和标题同行的说明文字 | +| noteMaxLine | int | 1 | 说明文字组件 最大行数 | +| noteMaxWidth | double? | - | 说明文字组件 最大宽度,超过部分显示省略号,防止文字溢出 | +| noteWidget | Widget? | - | 说明文字组件 | +| onClick | TCellClick? | - | 点击事件 | +| onLongPress | TCellClick? | - | 长按事件 | +| required | bool? | false | 是否显示表单必填星号 | +| rightIcon | IconData? | - | 最右侧图标 | +| rightIconWidget | Widget? | - | 最右侧图标组件 | +| showBottomBorder | bool? | false | 是否显示下边框(建议TCellGroup组件下false,避免与bordered重叠) | +| style | TCellStyle? | - | 自定义样式 | +| title | String? | - | 标题 | +| titleWidget | Widget? | - | 标题组件 | + +``` +``` + ### TCellGroup #### 简介 单元格组组件 @@ -52,42 +91,3 @@ | 名称 | 说明 | | --- | --- | | TCellStyle.cellStyle | 生成单元格默认样式 | - -``` -``` - -### TCell -#### 简介 -单元格组件 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| align | TCellAlign? | TCellAlign.middle | 内容的对齐方式,默认居中对齐。可选项:top/middle/bottom | -| arrow | bool? | false | 是否显示右侧箭头 | -| bordered | bool? | true | 是否显示下边框,仅在TCellGroup组件下起作用 | -| description | String? | - | 下方内容描述文字 | -| descriptionWidget | Widget? | - | 下方内容描述组件 | -| disabled | bool? | false | 禁用 | -| height | double? | - | 高度 | -| hover | bool? | true | 是否开启点击反馈 | -| image | ImageProvider? | - | 主图 | -| imageCircle | double? | 50 | 主图圆角,默认50(圆形) | -| imageSize | double? | - | 主图尺寸 | -| imageWidget | Widget? | - | 主图组件 | -| key | | - | | -| leftIcon | IconData? | - | 左侧图标,出现在单元格标题的左侧 | -| leftIconWidget | Widget? | - | 左侧图标组件 | -| note | String? | - | 和标题同行的说明文字 | -| noteMaxLine | int | 1 | 说明文字组件 最大行数 | -| noteMaxWidth | double? | - | 说明文字组件 最大宽度,超过部分显示省略号,防止文字溢出 | -| noteWidget | Widget? | - | 说明文字组件 | -| onClick | TCellClick? | - | 点击事件 | -| onLongPress | TCellClick? | - | 长按事件 | -| required | bool? | false | 是否显示表单必填星号 | -| rightIcon | IconData? | - | 最右侧图标 | -| rightIconWidget | Widget? | - | 最右侧图标组件 | -| showBottomBorder | bool? | false | 是否显示下边框(建议TCellGroup组件下false,避免与bordered重叠) | -| style | TCellStyle? | - | 自定义样式 | -| title | String? | - | 标题 | -| titleWidget | Widget? | - | 标题组件 | diff --git a/tdesign-component/example/assets/api/checkbox_api.md b/tdesign-component/example/assets/api/checkbox_api.md index 1683f7697..0d77c411c 100644 --- a/tdesign-component/example/assets/api/checkbox_api.md +++ b/tdesign-component/example/assets/api/checkbox_api.md @@ -1,26 +1,4 @@ ## API -### TCheckboxGroup -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| checkedIds | List? | - | 勾选的CheckBox id列表 | -| child | | - | | -| contentDirection | TContentDirection? | - | 文字相对icon的方位 | -| controller | TCheckboxGroupController? | - | 可以通过控制器操作勾选状态 | -| customContentBuilder | ContentBuilder? | - | CheckBox完全自定义内容 | -| customIconBuilder | IconBuilder? | - | 自定义选择icon的样式 | -| key | | - | | -| maxChecked | int? | - | 最多可以勾选多少 | -| onChangeGroup | OnGroupChange? | - | 状态变化监听器 | -| onOverloadChecked | VoidCallback? | - | 超过最大可勾选的个数 | -| spacing | double? | - | CheckBoxicon和文字的距离 | -| style | TCheckboxStyle? | - | CheckBox复选框样式:圆形或方形 | -| titleMaxLine | int? | - | CheckBox标题的行数 | - -``` -``` - ### TCheckbox #### 默认构造方法 @@ -53,3 +31,25 @@ | titleColor | Color? | - | 标题文字颜色 | | titleFont | Font? | - | 标题字体大小 | | titleMaxLine | int? | - | 标题的行数 | + +``` +``` + +### TCheckboxGroup +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| checkedIds | List? | - | 勾选的CheckBox id列表 | +| child | | - | | +| contentDirection | TContentDirection? | - | 文字相对icon的方位 | +| controller | TCheckboxGroupController? | - | 可以通过控制器操作勾选状态 | +| customContentBuilder | ContentBuilder? | - | CheckBox完全自定义内容 | +| customIconBuilder | IconBuilder? | - | 自定义选择icon的样式 | +| key | | - | | +| maxChecked | int? | - | 最多可以勾选多少 | +| onChangeGroup | OnGroupChange? | - | 状态变化监听器 | +| onOverloadChecked | VoidCallback? | - | 超过最大可勾选的个数 | +| spacing | double? | - | CheckBoxicon和文字的距离 | +| style | TCheckboxStyle? | - | CheckBox复选框样式:圆形或方形 | +| titleMaxLine | int? | - | CheckBox标题的行数 | diff --git a/tdesign-component/example/assets/api/dialog_api.md b/tdesign-component/example/assets/api/dialog_api.md index 1130c3590..9dd84999d 100644 --- a/tdesign-component/example/assets/api/dialog_api.md +++ b/tdesign-component/example/assets/api/dialog_api.md @@ -1,26 +1,37 @@ ## API -### TImageDialog +### TAlertDialog #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | backgroundColor | Color? | - | 背景颜色 | +| buttonStyle | | TDialogButtonStyle.normal | | | buttonWidget | Widget? | - | 自定义按钮 | | content | String? | - | 内容 | | contentColor | Color? | - | 内容颜色 | +| contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 | | contentWidget | Widget? | - | 内容Widget | -| image | Image | - | 图片 | -| imagePosition | TDialogImagePosition? | TDialogImagePosition.top | 图片位置 | | key | | - | | | leftBtn | TDialogButtonOptions? | - | 左侧按钮配置 | -| padding | EdgeInsets? | - | 内容内边距 | +| leftBtnAction | Function()? | - | 左侧按钮默认点击 | +| padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 | | radius | double | 12.0 | 圆角 | | rightBtn | TDialogButtonOptions? | - | 右侧按钮配置 | +| rightBtnAction | Function()? | - | 右侧按钮默认点击 | | showCloseButton | bool? | - | 显示右上角关闭按钮 | | title | String? | - | 标题 | | titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | | titleColor | Color? | - | 标题颜色 | + +#### 工厂构造方法 + +| 名称 | 说明 | +| --- | --- | +| TAlertDialog.vertical | 纵向按钮排列的对话框 + + [buttons]参数是必须的,纵向按钮默认样式都是[TButtonTheme.primary] | + ``` ``` @@ -52,6 +63,24 @@ ``` ``` +### TDialogButtonOptions +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| action | Function()? | - | 点击操作 | +| fontWeight | FontWeight? | - | 字体粗细 | +| height | double? | - | 按钮高度 | +| style | TButtonStyle? | - | 按钮样式 | +| theme | TButtonTheme? | - | 按钮类型 | +| title | String | - | 标题内容 | +| titleColor | Color? | - | 标题颜色 | +| titleSize | double? | - | 字体大小 | +| type | TButtonType? | - | 按钮类型 | + +``` +``` + ### TDialogScaffold #### 默认构造方法 @@ -154,57 +183,28 @@ ``` ``` -### TDialogButtonOptions -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| action | Function()? | - | 点击操作 | -| fontWeight | FontWeight? | - | 字体粗细 | -| height | double? | - | 按钮高度 | -| style | TButtonStyle? | - | 按钮样式 | -| theme | TButtonTheme? | - | 按钮类型 | -| title | String | - | 标题内容 | -| titleColor | Color? | - | 标题颜色 | -| titleSize | double? | - | 字体大小 | -| type | TButtonType? | - | 按钮类型 | - -``` -``` - -### TAlertDialog +### TImageDialog #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | backgroundColor | Color? | - | 背景颜色 | -| buttonStyle | | TDialogButtonStyle.normal | | | buttonWidget | Widget? | - | 自定义按钮 | | content | String? | - | 内容 | | contentColor | Color? | - | 内容颜色 | -| contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 | | contentWidget | Widget? | - | 内容Widget | +| image | Image | - | 图片 | +| imagePosition | TDialogImagePosition? | TDialogImagePosition.top | 图片位置 | | key | | - | | | leftBtn | TDialogButtonOptions? | - | 左侧按钮配置 | -| leftBtnAction | Function()? | - | 左侧按钮默认点击 | -| padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 | +| padding | EdgeInsets? | - | 内容内边距 | | radius | double | 12.0 | 圆角 | | rightBtn | TDialogButtonOptions? | - | 右侧按钮配置 | -| rightBtnAction | Function()? | - | 右侧按钮默认点击 | | showCloseButton | bool? | - | 显示右上角关闭按钮 | | title | String? | - | 标题 | | titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | | titleColor | Color? | - | 标题颜色 | - -#### 工厂构造方法 - -| 名称 | 说明 | -| --- | --- | -| TAlertDialog.vertical | 纵向按钮排列的对话框 - - [buttons]参数是必须的,纵向按钮默认样式都是[TButtonTheme.primary] | - ``` ``` diff --git a/tdesign-component/example/assets/api/drawer_api.md b/tdesign-component/example/assets/api/drawer_api.md index 2f29fa229..6dcef5beb 100644 --- a/tdesign-component/example/assets/api/drawer_api.md +++ b/tdesign-component/example/assets/api/drawer_api.md @@ -1,66 +1,66 @@ ## API -### TDrawerWidget +### TDrawer #### 简介 -抽屉内容组件 - 可用于 Scaffold 中的 drawer 属性 +抽屉组件 #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | backgroundColor | Color? | - | 组件背景颜色 | | bordered | bool? | true | 是否显示边框 | +| closeOnOverlayClick | bool? | true | 点击蒙层时是否关闭抽屉 | | contentWidget | Widget? | - | 自定义内容,优先级高于[items]/[footer]/[title] | +| context | BuildContext | context | 上下文 | +| drawerTop | double? | - | 距离顶部的距离 | | footer | Widget? | - | 抽屉的底部 | | hover | bool? | true | 是否开启点击反馈 | | isShowLastBordered | bool? | true | 是否显示最后一行分割线 | | items | List? | - | 抽屉里的列表项 | -| key | | - | | +| onClose | VoidCallback? | - | 关闭时触发 | | onItemClick | TDrawerItemClickCallback? | - | 点击抽屉里的列表项触发 | +| placement | TDrawerPlacement? | TDrawerPlacement.right | 抽屉方向 | +| showOverlay | bool? | true | 是否显示遮罩层 | | style | TCellStyle? | - | 列表自定义样式 | | title | String? | - | 抽屉的标题 | | titleWidget | Widget? | - | 抽屉的标题组件 | +| visible | bool? | - | 组件是否可见 | | width | double? | 280 | 宽度 | ``` ``` -### TDrawerItem -#### 简介 -抽屉里的列表项 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| content | Widget? | - | 完全自定义 | -| icon | Widget? | - | 每列图标 | -| title | String? | - | 每列标题 | - -``` -``` - -### TDrawer +### TDrawerWidget #### 简介 -抽屉组件 +抽屉内容组件 + 可用于 Scaffold 中的 drawer 属性 #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | backgroundColor | Color? | - | 组件背景颜色 | | bordered | bool? | true | 是否显示边框 | -| closeOnOverlayClick | bool? | true | 点击蒙层时是否关闭抽屉 | | contentWidget | Widget? | - | 自定义内容,优先级高于[items]/[footer]/[title] | -| context | BuildContext | context | 上下文 | -| drawerTop | double? | - | 距离顶部的距离 | | footer | Widget? | - | 抽屉的底部 | | hover | bool? | true | 是否开启点击反馈 | | isShowLastBordered | bool? | true | 是否显示最后一行分割线 | | items | List? | - | 抽屉里的列表项 | -| onClose | VoidCallback? | - | 关闭时触发 | +| key | | - | | | onItemClick | TDrawerItemClickCallback? | - | 点击抽屉里的列表项触发 | -| placement | TDrawerPlacement? | TDrawerPlacement.right | 抽屉方向 | -| showOverlay | bool? | true | 是否显示遮罩层 | | style | TCellStyle? | - | 列表自定义样式 | | title | String? | - | 抽屉的标题 | | titleWidget | Widget? | - | 抽屉的标题组件 | -| visible | bool? | - | 组件是否可见 | | width | double? | 280 | 宽度 | + +``` +``` + +### TDrawerItem +#### 简介 +抽屉里的列表项 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| content | Widget? | - | 完全自定义 | +| icon | Widget? | - | 每列图标 | +| title | String? | - | 每列标题 | diff --git a/tdesign-component/example/assets/api/dropdown-menu_api.md b/tdesign-component/example/assets/api/dropdown-menu_api.md index 8b38e08b8..e19bc36b7 100644 --- a/tdesign-component/example/assets/api/dropdown-menu_api.md +++ b/tdesign-component/example/assets/api/dropdown-menu_api.md @@ -1,7 +1,29 @@ ## API -### TDropdownItemController +### TDropdownMenu #### 简介 -下拉菜单控制器 +下拉菜单 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| arrowColor | Color? | - | 自定义箭头颜色 | +| arrowIcon | IconData? | - | 自定义箭头图标 | +| builder | TDropdownItemBuilder? | - | 下拉菜单构建器,优先级高于[items] | +| closeOnClickOverlay | bool? | true | 是否在点击遮罩层后关闭菜单 | +| decoration | Decoration? | - | 下拉菜单的装饰器 | +| direction | TDropdownMenuDirection? | TDropdownMenuDirection.auto | 菜单展开方向(down、up、auto) | +| duration | double? | 200.0 | 动画时长,毫秒 | +| height | double? | 48 | menu的高度 | +| isScrollable | bool? | false | 是否开启滚动列表 | +| items | List? | - | 下拉菜单 | +| key | | - | | +| labelBuilder | LabelBuilder? | - | 自定义标签内容 | +| onMenuClosed | ValueChanged? | - | 关闭菜单事件 | +| onMenuOpened | ValueChanged? | - | 展开菜单事件 | +| showOverlay | bool? | true | 是否显示遮罩层 | +| tabBarAlign | MainAxisAlignment? | MainAxisAlignment.center | [TDropdownItem.label]和[arrowIcon]/[TDropdownItem.arrowIcon]的对齐方式 | +| width | double? | - | menu的宽度 | + ``` ``` @@ -52,27 +74,6 @@ ``` ``` -### TDropdownMenu +### TDropdownItemController #### 简介 -下拉菜单 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| arrowColor | Color? | - | 自定义箭头颜色 | -| arrowIcon | IconData? | - | 自定义箭头图标 | -| builder | TDropdownItemBuilder? | - | 下拉菜单构建器,优先级高于[items] | -| closeOnClickOverlay | bool? | true | 是否在点击遮罩层后关闭菜单 | -| decoration | Decoration? | - | 下拉菜单的装饰器 | -| direction | TDropdownMenuDirection? | TDropdownMenuDirection.auto | 菜单展开方向(down、up、auto) | -| duration | double? | 200.0 | 动画时长,毫秒 | -| height | double? | 48 | menu的高度 | -| isScrollable | bool? | false | 是否开启滚动列表 | -| items | List? | - | 下拉菜单 | -| key | | - | | -| labelBuilder | LabelBuilder? | - | 自定义标签内容 | -| onMenuClosed | ValueChanged? | - | 关闭菜单事件 | -| onMenuOpened | ValueChanged? | - | 展开菜单事件 | -| showOverlay | bool? | true | 是否显示遮罩层 | -| tabBarAlign | MainAxisAlignment? | MainAxisAlignment.center | [TDropdownItem.label]和[arrowIcon]/[TDropdownItem.arrowIcon]的对齐方式 | -| width | double? | - | menu的宽度 | +下拉菜单控制器 \ No newline at end of file diff --git a/tdesign-component/example/assets/api/form_api.md b/tdesign-component/example/assets/api/form_api.md index 9ed8958ae..61922773d 100644 --- a/tdesign-component/example/assets/api/form_api.md +++ b/tdesign-component/example/assets/api/form_api.md @@ -1,4 +1,33 @@ ## API +### TForm +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| btnGroup | List? | - | 表单按钮组 | +| colon | bool? | false | 是否在表单标签字段右侧显示冒号 | +| data | Map | - | 表单数据 | +| disabled | bool | false | 是否禁用整个表单 | +| errorMessage | Object? | - | 表单信息错误信息配置 | +| formContentAlign | TextAlign | TextAlign.left | 表单内容对齐方式: 左对齐、右对齐、居中对齐 | +| formController | FormController? | - | 表单控制器 | +| formLabelAlign | TextAlign? | TextAlign.left | 表单字段标签的对齐方式: | +| formShowErrorMessage | bool? | true | 校验不通过时,是否显示错误提示信息,统一控制全部表单项 | +| isHorizontal | bool | true | 表单排列方式是否为 水平方向 | +| items | List | - | 表单内容 items | +| key | | - | | +| labelWidth | double? | 20.0 | 可以整体设置 label 标签宽度 | +| onReset | Function? | - | 表单重置时触发 | +| onSubmit | Function | - | 表单提交时触发 | +| preventSubmitDefault | bool? | true | 是否阻止表单提交默认事件(表单提交默认事件会刷新页面) | +| requiredMark | bool? | true | 是否显示必填符号(*),默认显示 | +| rules | Map | - | 整个表单字段校验规则 | +| scrollToFirstError | String? | - | 表单校验不通过时,是否自动滚动到第一个校验不通过的字段,平滑滚动或是瞬间直达。 | +| submitWithWarningMessage | bool? | false | 【讨论中】当校验结果只有告警信息时,是否触发 submit 提交事件 | + +``` +``` + ### TFormItem #### 默认构造方法 @@ -39,32 +68,3 @@ | errorMessage | String | - | 错误提示信息 | | type | TFormItemType | - | 校验对象的类型 | | validate | String? Function(dynamic) | - | 校验方法 | - -``` -``` - -### TForm -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| btnGroup | List? | - | 表单按钮组 | -| colon | bool? | false | 是否在表单标签字段右侧显示冒号 | -| data | Map | - | 表单数据 | -| disabled | bool | false | 是否禁用整个表单 | -| errorMessage | Object? | - | 表单信息错误信息配置 | -| formContentAlign | TextAlign | TextAlign.left | 表单内容对齐方式: 左对齐、右对齐、居中对齐 | -| formController | FormController? | - | 表单控制器 | -| formLabelAlign | TextAlign? | TextAlign.left | 表单字段标签的对齐方式: | -| formShowErrorMessage | bool? | true | 校验不通过时,是否显示错误提示信息,统一控制全部表单项 | -| isHorizontal | bool | true | 表单排列方式是否为 水平方向 | -| items | List | - | 表单内容 items | -| key | | - | | -| labelWidth | double? | 20.0 | 可以整体设置 label 标签宽度 | -| onReset | Function? | - | 表单重置时触发 | -| onSubmit | Function | - | 表单提交时触发 | -| preventSubmitDefault | bool? | true | 是否阻止表单提交默认事件(表单提交默认事件会刷新页面) | -| requiredMark | bool? | true | 是否显示必填符号(*),默认显示 | -| rules | Map | - | 整个表单字段校验规则 | -| scrollToFirstError | String? | - | 表单校验不通过时,是否自动滚动到第一个校验不通过的字段,平滑滚动或是瞬间直达。 | -| submitWithWarningMessage | bool? | false | 【讨论中】当校验结果只有告警信息时,是否触发 submit 提交事件 | diff --git a/tdesign-component/example/assets/api/image-viewer_api.md b/tdesign-component/example/assets/api/image-viewer_api.md index 6eecfe432..f36abefb8 100644 --- a/tdesign-component/example/assets/api/image-viewer_api.md +++ b/tdesign-component/example/assets/api/image-viewer_api.md @@ -1,4 +1,15 @@ ## API +### TImageViewer + +#### 静态方法 + +| 名称 | 返回类型 | 参数 | 说明 | +| --- | --- | --- | --- | +| showImageViewer | | required BuildContext context, required List images, List? labels, bool? closeBtn, bool? deleteBtn, bool? showIndex, bool? loop, bool? autoplay, int? duration, Color? bgColor, Color? navBarBgColor, Color? iconColor, TextStyle? labelStyle, TextStyle? indexStyle, Color? modalBarrierColor, bool? barrierDismissible, int? defaultIndex, double? width, double? height, OnIndexChange? onIndexChange, OnClose? onClose, OnDelete? onDelete, bool? ignoreDeleteError, OnImageTap? onTap, OnLongPress? onLongPress, LeftItemBuilder? leftItemBuilder, RightItemBuilder? rightItemBuilder, | 显示图片预览 | + +``` +``` + ### TImageViewerWidget #### 默认构造方法 @@ -29,14 +40,3 @@ | rightItemBuilder | RightItemBuilder? | - | 右侧自定义操作 | | showIndex | bool? | - | 是否显示页码 | | width | double? | - | 图片宽度 | - -``` -``` - -### TImageViewer - -#### 静态方法 - -| 名称 | 返回类型 | 参数 | 说明 | -| --- | --- | --- | --- | -| showImageViewer | | required BuildContext context, required List images, List? labels, bool? closeBtn, bool? deleteBtn, bool? showIndex, bool? loop, bool? autoplay, int? duration, Color? bgColor, Color? navBarBgColor, Color? iconColor, TextStyle? labelStyle, TextStyle? indexStyle, Color? modalBarrierColor, bool? barrierDismissible, int? defaultIndex, double? width, double? height, OnIndexChange? onIndexChange, OnClose? onClose, OnDelete? onDelete, bool? ignoreDeleteError, OnImageTap? onTap, OnLongPress? onLongPress, LeftItemBuilder? leftItemBuilder, RightItemBuilder? rightItemBuilder, | 显示图片预览 | diff --git a/tdesign-component/example/assets/api/indexes_api.md b/tdesign-component/example/assets/api/indexes_api.md index 81168ad22..eeb9a7294 100644 --- a/tdesign-component/example/assets/api/indexes_api.md +++ b/tdesign-component/example/assets/api/indexes_api.md @@ -1,4 +1,28 @@ ## API +### TIndexes +#### 简介 +索引 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| builderAnchor | Widget? Function(BuildContext context, String index, bool isPinnedToTop)? | - | 锚点自定义构建 | +| builderContent | Widget? Function(BuildContext context, String index) | - | 内容自定义构建 | +| builderIndex | Widget Function(BuildContext context, String index, bool isActive)? | - | 索引文本自定义构建,包括索引激活左侧提示 | +| capsuleTheme | bool? | false | 锚点是否为胶囊式样式 | +| indexList | List? | - | 索引字符列表。不传默认 A-Z | +| indexListMaxHeight | double? | 0.8 | 索引列表最大高度(父容器高度的百分比,默认 0.8) | +| key | | - | | +| onChange | void Function(String index)? | - | 索引发生变更时触发事件 | +| onSelect | void Function(String index)? | - | 点击侧边栏时触发事件 | +| reverse | bool? | false | 反方向滚动置顶 | +| scrollController | ScrollController? | - | 滚动控制器 | +| sticky | bool? | true | 锚点是否吸顶 | +| stickyOffset | double? | 0 | 锚点吸顶时与顶部的距离 | + +``` +``` + ### TIndexesAnchor #### 简介 索引锚点 @@ -29,27 +53,3 @@ | indexListMaxHeight | double | 0.8 | 索引列表最大高度(父容器高度的百分比,默认0.8) | | key | | - | | | onSelect | void Function(String newIndex, String oldIndex) | - | 点击侧边栏时触发事件 | - -``` -``` - -### TIndexes -#### 简介 -索引 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| builderAnchor | Widget? Function(BuildContext context, String index, bool isPinnedToTop)? | - | 锚点自定义构建 | -| builderContent | Widget? Function(BuildContext context, String index) | - | 内容自定义构建 | -| builderIndex | Widget Function(BuildContext context, String index, bool isActive)? | - | 索引文本自定义构建,包括索引激活左侧提示 | -| capsuleTheme | bool? | false | 锚点是否为胶囊式样式 | -| indexList | List? | - | 索引字符列表。不传默认 A-Z | -| indexListMaxHeight | double? | 0.8 | 索引列表最大高度(父容器高度的百分比,默认 0.8) | -| key | | - | | -| onChange | void Function(String index)? | - | 索引发生变更时触发事件 | -| onSelect | void Function(String index)? | - | 点击侧边栏时触发事件 | -| reverse | bool? | false | 反方向滚动置顶 | -| scrollController | ScrollController? | - | 滚动控制器 | -| sticky | bool? | true | 锚点是否吸顶 | -| stickyOffset | double? | 0 | 锚点吸顶时与顶部的距离 | diff --git a/tdesign-component/example/assets/api/message_api.md b/tdesign-component/example/assets/api/message_api.md index 6189b22e5..a327735d1 100644 --- a/tdesign-component/example/assets/api/message_api.md +++ b/tdesign-component/example/assets/api/message_api.md @@ -1,28 +1,4 @@ ## API -### MessageLink -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| color | Color? | - | 颜色 | -| name | String | - | 名称 | -| uri | Uri? | - | 资源链接 | - -``` -``` - -### MessageMarquee -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| delay | int? | - | 延迟时间(毫秒) | -| loop | int? | - | 循环次数 | -| speed | int? | - | 速度 | - -``` -``` - ### TMessage #### 默认构造方法 @@ -48,3 +24,27 @@ | 名称 | 返回类型 | 参数 | 说明 | | --- | --- | --- | --- | | showMessage | | required BuildContext context, String? content, bool? visible, int? duration, dynamic closeBtn, dynamic icon, dynamic link, MessageMarquee? marquee, List? offset, MessageTheme? theme, VoidCallback? onCloseBtnClick, VoidCallback? onDurationEnd, VoidCallback? onLinkClick, | | + +``` +``` + +### MessageMarquee +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| delay | int? | - | 延迟时间(毫秒) | +| loop | int? | - | 循环次数 | +| speed | int? | - | 速度 | + +``` +``` + +### MessageLink +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| color | Color? | - | 颜色 | +| name | String | - | 名称 | +| uri | Uri? | - | 资源链接 | diff --git a/tdesign-component/example/assets/api/notice-bar_api.md b/tdesign-component/example/assets/api/notice-bar_api.md index d79c2e5f3..f43768900 100644 --- a/tdesign-component/example/assets/api/notice-bar_api.md +++ b/tdesign-component/example/assets/api/notice-bar_api.md @@ -1,28 +1,4 @@ ## API -### TNoticeBarStyle -#### 简介 -公告栏样式 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| backgroundColor | Color? | - | 公告栏背景色 | -| context | BuildContext? | - | 上下文 | -| leftIconColor | Color? | - | 公告栏左侧图标颜色 | -| padding | EdgeInsetsGeometry? | - | 公告栏内边距 | -| rightIconColor | Color? | - | 公告栏右侧图标颜色 | -| textStyle | TextStyle? | - | 公告栏内容样式 | - - -#### 工厂构造方法 - -| 名称 | 说明 | -| --- | --- | -| TNoticeBarStyle.generateTheme | 根据主题生成样式 | - -``` -``` - ### TNoticeBar #### 简介 @@ -46,3 +22,27 @@ | style | TNoticeBarStyle? | - | 公告栏样式 [TNoticeBarStyle] | | suffixIcon | IconData? | - | 右侧图标 | | theme | TNoticeBarTheme? | TNoticeBarTheme.info | 主题 | + +``` +``` + +### TNoticeBarStyle +#### 简介 +公告栏样式 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| backgroundColor | Color? | - | 公告栏背景色 | +| context | BuildContext? | - | 上下文 | +| leftIconColor | Color? | - | 公告栏左侧图标颜色 | +| padding | EdgeInsetsGeometry? | - | 公告栏内边距 | +| rightIconColor | Color? | - | 公告栏右侧图标颜色 | +| textStyle | TextStyle? | - | 公告栏内容样式 | + + +#### 工厂构造方法 + +| 名称 | 说明 | +| --- | --- | +| TNoticeBarStyle.generateTheme | 根据主题生成样式 | diff --git a/tdesign-component/example/assets/api/picker_api.md b/tdesign-component/example/assets/api/picker_api.md index f1645a9fb..d09bcdf8f 100644 --- a/tdesign-component/example/assets/api/picker_api.md +++ b/tdesign-component/example/assets/api/picker_api.md @@ -1,4 +1,29 @@ ## API +### TPicker +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| cancel | Widget? | - | 工具栏左侧自定义插槽,默认使用 [TResourceDelegate.cancel] | +| confirm | Widget? | - | 工具栏右侧自定义插槽,默认使用 [TResourceDelegate.confirm] | +| disabled | bool | false | 是否禁用整个选择器(禁止滚动和操作),默认 false | +| height | double | 200 | 视窗高度,默认 200 | +| initialValue | List? | - | 初始选中值列表(按 value 匹配) | +| itemBuilder | ItemBuilderType? | - | 自定义子项构建器(disabled 项仍由内部统一渲染,不会走此 builder) | +| itemCount | int | 5 | 每屏显示 item 数,默认 5 | +| itemDistanceCalculator | ItemDistanceCalculator? | - | 自定义距离计算器(控制颜色/字重/字号随"离中心距离"的变化) | +| items | TPickerItems | - | 数据源(必填) | +| key | | - | | +| onCancel | VoidCallback? | - | 点击「取消」按钮回调 | +| onChange | void Function(TPickerValue)? | - | 值改变回调(滚动时实时触发) | +| onConfirm | void Function(TPickerValue)? | - | 点击「确认」按钮回调 | +| onLoad | void Function(TPickerLoadEvent)? | - | 列选中项变化的事件回调 | +| title | String? | - | 工具栏中部标题(可选,不传时中部留白) | +| titleWidget | Widget? | - | 工具栏中部自定义标题插槽 | + +``` +``` + ### TPickerOption #### 默认构造方法 @@ -22,32 +47,16 @@ ``` ``` -### TPicker +### TPickerLoadEvent #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| cancel | Widget? | - | 工具栏左侧自定义插槽,默认使用 [TResourceDelegate.cancel] | -| confirm | Widget? | - | 工具栏右侧自定义插槽,默认使用 [TResourceDelegate.confirm] | -| disabled | bool | false | 是否禁用整个选择器(禁止滚动和操作),默认 false | -| height | double | 200 | 视窗高度,默认 200 | -| initialValue | List? | - | 初始选中值列表(按 value 匹配) | -| itemBuilder | ItemBuilderType? | - | 自定义子项构建器(disabled 项仍由内部统一渲染,不会走此 builder) | -| itemCount | int | 5 | 每屏显示 item 数,默认 5 | -| itemDistanceCalculator | ItemDistanceCalculator? | - | 自定义距离计算器(控制颜色/字重/字号随"离中心距离"的变化) | -| items | TPickerItems | - | 数据源(必填) | -| key | | - | | -| onCancel | VoidCallback? | - | 点击「取消」按钮回调 | -| onChange | void Function(TPickerValue)? | - | 值改变回调(滚动时实时触发) | -| onConfirm | void Function(TPickerValue)? | - | 点击「确认」按钮回调 | -| onLoad | void Function(TPickerLoadEvent)? | - | 列选中项变化的事件回调 | -| title | String? | - | 工具栏中部标题(可选,不传时中部留白) | -| titleWidget | Widget? | - | 工具栏中部自定义标题插槽 | - -``` -``` +| column | int | - | 触发事件的列索引(0 表示第一列) | +| displayedCount | int | - | 当前列已展示的选项总数 | +| parentValue | dynamic | - | 当前列的父级选中值(联动模式下使用) | +| remaining | int | - | 距底部剩余的选项数(业务可用此值做"接近底部时加载"判断) | -### TPickerItems ``` ``` @@ -99,6 +108,10 @@ ``` ``` +### TPickerItems +``` +``` + ### TPickerKeys #### 默认构造方法 @@ -108,16 +121,3 @@ | disabled | String | 'disabled' | 禁用标记对应的字段名,默认 `disabled` | | label | String | 'label' | 展示文案对应的字段名,默认 `label` | | value | String | 'value' | 业务值对应的字段名,默认 `value` | - -``` -``` - -### TPickerLoadEvent -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| column | int | - | 触发事件的列索引(0 表示第一列) | -| displayedCount | int | - | 当前列已展示的选项总数 | -| parentValue | dynamic | - | 当前列的父级选中值(联动模式下使用) | -| remaining | int | - | 距底部剩余的选项数(业务可用此值做"接近底部时加载"判断) | diff --git a/tdesign-component/example/assets/api/popover_api.md b/tdesign-component/example/assets/api/popover_api.md index a21397791..0a44f139d 100644 --- a/tdesign-component/example/assets/api/popover_api.md +++ b/tdesign-component/example/assets/api/popover_api.md @@ -1,4 +1,17 @@ ## API +### TPopover +#### 简介 + + +#### 静态方法 + +| 名称 | 返回类型 | 参数 | 说明 | +| --- | --- | --- | --- | +| showPopover | | required BuildContext context, String? content, Widget? contentWidget, double offset, TPopoverTheme? theme, bool closeOnClickOutside, TPopoverPlacement? placement, bool? showArrow, double arrowSize, EdgeInsetsGeometry? padding, double? width, double? height, Color? overlayColor, OnTap? onTap, OnLongTap? onLongTap, BorderRadius? radius, | | + +``` +``` + ### TPopoverWidget #### 简介 @@ -21,16 +34,3 @@ | showArrow | bool? | true | 是否显示浮层箭头 | | theme | TPopoverTheme? | - | 弹出气泡主题 | | width | double? | - | 内容宽度(包含padding,实际高度:height - paddingLeft - paddingRight) | - -``` -``` - -### TPopover -#### 简介 - - -#### 静态方法 - -| 名称 | 返回类型 | 参数 | 说明 | -| --- | --- | --- | --- | -| showPopover | | required BuildContext context, String? content, Widget? contentWidget, double offset, TPopoverTheme? theme, bool closeOnClickOutside, TPopoverPlacement? placement, bool? showArrow, double arrowSize, EdgeInsetsGeometry? padding, double? width, double? height, Color? overlayColor, OnTap? onTap, OnLongTap? onLongTap, BorderRadius? radius, | | diff --git a/tdesign-component/example/assets/api/popup_api.md b/tdesign-component/example/assets/api/popup_api.md index fa6674dd6..8ad1680cc 100644 --- a/tdesign-component/example/assets/api/popup_api.md +++ b/tdesign-component/example/assets/api/popup_api.md @@ -1,4 +1,30 @@ ## API +### TSlidePopupRoute +#### 简介 +从屏幕的某个方向滑动弹出的Dialog框的路由,比如从顶部、底部、左、右滑出页面 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| barrierClick | VoidCallback? | - | 蒙层点击事件,仅在[modalBarrierFull]为false时触发 | +| barrierLabel | | - | | +| builder | WidgetBuilder | - | 控件构建器 | +| close | VoidCallback? | - | 关闭前事件 | +| focusMove | bool | false | 是否有输入框获取焦点时整体平移避免输入框被遮挡 | +| isDismissible | bool | true | 点击蒙层能否关闭 | +| modalBarrierColor | Color? | Colors.black54 | 蒙层颜色 | +| modalBarrierFull | bool | false | 是否全屏显示蒙层 | +| modalHeight | double? | - | 弹出框高度 | +| modalLeft | double? | 0 | 弹出框左侧距离 | +| modalTop | double? | 0 | 弹出框顶部距离 | +| modalWidth | double? | - | 弹出框宽度 | +| open | VoidCallback? | - | 打开前事件 | +| opened | VoidCallback? | - | 打开后事件 | +| slideTransitionFrom | SlideTransitionFrom | SlideTransitionFrom.bottom | 设置从屏幕的哪个方向滑出 | + +``` +``` + ### TPopupBottomDisplayPanel #### 简介 右上角带关闭的底部浮层面板 @@ -69,29 +95,3 @@ | closeUnderBottom | bool | false | 关闭按钮是否在视图框下方 | | key | | - | | | radius | double? | - | 圆角 | - -``` -``` - -### TSlidePopupRoute -#### 简介 -从屏幕的某个方向滑动弹出的Dialog框的路由,比如从顶部、底部、左、右滑出页面 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| barrierClick | VoidCallback? | - | 蒙层点击事件,仅在[modalBarrierFull]为false时触发 | -| barrierLabel | | - | | -| builder | WidgetBuilder | - | 控件构建器 | -| close | VoidCallback? | - | 关闭前事件 | -| focusMove | bool | false | 是否有输入框获取焦点时整体平移避免输入框被遮挡 | -| isDismissible | bool | true | 点击蒙层能否关闭 | -| modalBarrierColor | Color? | Colors.black54 | 蒙层颜色 | -| modalBarrierFull | bool | false | 是否全屏显示蒙层 | -| modalHeight | double? | - | 弹出框高度 | -| modalLeft | double? | 0 | 弹出框左侧距离 | -| modalTop | double? | 0 | 弹出框顶部距离 | -| modalWidth | double? | - | 弹出框宽度 | -| open | VoidCallback? | - | 打开前事件 | -| opened | VoidCallback? | - | 打开后事件 | -| slideTransitionFrom | SlideTransitionFrom | SlideTransitionFrom.bottom | 设置从屏幕的哪个方向滑出 | diff --git a/tdesign-component/example/assets/api/side-bar_api.md b/tdesign-component/example/assets/api/side-bar_api.md index 771c48523..d36520c3d 100644 --- a/tdesign-component/example/assets/api/side-bar_api.md +++ b/tdesign-component/example/assets/api/side-bar_api.md @@ -1,20 +1,4 @@ ## API -### TSideBarItem -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| badge | TBadge? | - | 徽标 | -| disabled | bool | false | 是否禁用 | -| icon | IconData? | - | 图标 | -| key | | - | | -| label | String | '' | 标签 | -| textStyle | TextStyle? | - | 标签样式 | -| value | int | -1 | 值 | - -``` -``` - ### TSideBar #### 默认构造方法 @@ -37,3 +21,19 @@ | unSelectedBgColor | Color? | - | 未选择的背景颜色 | | unSelectedColor | Color? | - | 未选中颜色 | | value | int? | - | 选项值 | + +``` +``` + +### TSideBarItem +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| badge | TBadge? | - | 徽标 | +| disabled | bool | false | 是否禁用 | +| icon | IconData? | - | 图标 | +| key | | - | | +| label | String | '' | 标签 | +| textStyle | TextStyle? | - | 标签样式 | +| value | int | -1 | 值 | diff --git a/tdesign-component/example/assets/api/skeleton_api.md b/tdesign-component/example/assets/api/skeleton_api.md index 2564fd965..7801ab935 100644 --- a/tdesign-component/example/assets/api/skeleton_api.md +++ b/tdesign-component/example/assets/api/skeleton_api.md @@ -1,4 +1,24 @@ ## API +### TSkeleton +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| animation | TSkeletonAnimation? | - | 动画效果 | +| delay | int | 0 | 延迟显示加载时间 | +| key | | - | | +| theme | | TSkeletonTheme.text | | + + +#### 工厂构造方法 + +| 名称 | 说明 | +| --- | --- | +| TSkeleton.fromRowCol | 从行列框架创建骨架屏 | + +``` +``` + ### TSkeletonRowColStyle #### 默认构造方法 @@ -61,23 +81,3 @@ | TSkeletonRowColObj.rect | 矩形 | | TSkeletonRowColObj.spacer | 空白占位符 | | TSkeletonRowColObj.text | 文本 | - -``` -``` - -### TSkeleton -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| animation | TSkeletonAnimation? | - | 动画效果 | -| delay | int | 0 | 延迟显示加载时间 | -| key | | - | | -| theme | | TSkeletonTheme.text | | - - -#### 工厂构造方法 - -| 名称 | 说明 | -| --- | --- | -| TSkeleton.fromRowCol | 从行列框架创建骨架屏 | diff --git a/tdesign-component/example/assets/api/steps_api.md b/tdesign-component/example/assets/api/steps_api.md index 20601365b..62115981f 100644 --- a/tdesign-component/example/assets/api/steps_api.md +++ b/tdesign-component/example/assets/api/steps_api.md @@ -1,19 +1,4 @@ ## API -### TStepsItemData -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| content | String? | - | 内容 | -| customContent | Widget? | - | 自定义内容 | -| customTitle | Widget? | - | 自定义标题 | -| errorIcon | IconData? | - | 失败图标 | -| successIcon | IconData? | - | 成功图标 | -| title | String? | - | 标题 | - -``` -``` - ### TSteps #### 默认构造方法 @@ -27,3 +12,18 @@ | status | TStepsStatus | TStepsStatus.success | 步骤条状态 | | steps | List | - | 步骤条数据 | | verticalSelect | bool | false | 步骤条垂直自定义步骤条选择模式 | + +``` +``` + +### TStepsItemData +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| content | String? | - | 内容 | +| customContent | Widget? | - | 自定义内容 | +| customTitle | Widget? | - | 自定义标题 | +| errorIcon | IconData? | - | 失败图标 | +| successIcon | IconData? | - | 成功图标 | +| title | String? | - | 标题 | diff --git a/tdesign-component/example/assets/api/swiper_api.md b/tdesign-component/example/assets/api/swiper_api.md index 29b0af3fb..5a23bdff5 100644 --- a/tdesign-component/example/assets/api/swiper_api.md +++ b/tdesign-component/example/assets/api/swiper_api.md @@ -1,4 +1,19 @@ ## API +### TSwiperPagination +#### 简介 +TDesign风格的Swiper指示器样式,与flutter_swiper的Swiper结合使用 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| alignment | Alignment? | - | 当 scrollDirection== Axis.horizontal 时,默认Alignment.bottomCenter | +| builder | SwiperPlugin | TSwiperPagination.dots | 具体样式 | +| key | Key? | - | | +| margin | EdgeInsetsGeometry | const EdgeInsets.all(10.0) | 指示器和container之间的距离 | + +``` +``` + ### TPageTransformer #### 简介 TD默认PageTransformer @@ -17,18 +32,3 @@ TD默认PageTransformer | --- | --- | | TPageTransformer.margin | 普通margin的卡片式 | | TPageTransformer.scaleAndFade | 缩放或透明的卡片式 | - -``` -``` - -### TSwiperPagination -#### 简介 -TDesign风格的Swiper指示器样式,与flutter_swiper的Swiper结合使用 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| alignment | Alignment? | - | 当 scrollDirection== Axis.horizontal 时,默认Alignment.bottomCenter | -| builder | SwiperPlugin | TSwiperPagination.dots | 具体样式 | -| key | Key? | - | | -| margin | EdgeInsetsGeometry | const EdgeInsets.all(10.0) | 指示器和container之间的距离 | diff --git a/tdesign-component/example/assets/api/tab-bar_api.md b/tdesign-component/example/assets/api/tab-bar_api.md index 84f4e5365..dad57643b 100644 --- a/tdesign-component/example/assets/api/tab-bar_api.md +++ b/tdesign-component/example/assets/api/tab-bar_api.md @@ -1,36 +1,4 @@ ## API -### BadgeConfig -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| badgeRightOffset | double? | - | 消息右侧偏移量 | -| badgeTopOffset | double? | - | 消息顶部偏移量 | -| showBadge | bool | - | 是否展示消息 | -| tBadge | TBadge? | - | 消息样式(未设置但 showBadge 为 true,则默认使用红点) | - -``` -``` - -### TBottomTabBarTabConfig -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| allowMultipleTaps | bool | false | onTap 方法允许点击多次 | -| badgeConfig | BadgeConfig? | - | 消息配置 | -| onLongPress | GestureLongPressCallback? | - | 长按事件 | -| onTap | GestureTapCallback? | - | tab点击事件 | -| popUpButtonConfig | TBottomTabBarPopUpBtnConfig? | - | 弹窗配置 | -| selectedIcon | Widget? | - | 选中时图标 | -| selectTabTextStyle | TextStyle? | - | 文本已选择样式 basicType为text时必填 | -| tabText | String? | - | tab 文本 | -| unselectedIcon | Widget? | - | 未选中时图标 | -| unselectTabTextStyle | TextStyle? | - | 文本未选择样式 basicType为text时必填 | - -``` -``` - ### TBottomTabBar #### 默认构造方法 @@ -63,6 +31,38 @@ ``` ``` +### BadgeConfig +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| badgeRightOffset | double? | - | 消息右侧偏移量 | +| badgeTopOffset | double? | - | 消息顶部偏移量 | +| showBadge | bool | - | 是否展示消息 | +| tBadge | TBadge? | - | 消息样式(未设置但 showBadge 为 true,则默认使用红点) | + +``` +``` + +### TBottomTabBarTabConfig +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| allowMultipleTaps | bool | false | onTap 方法允许点击多次 | +| badgeConfig | BadgeConfig? | - | 消息配置 | +| onLongPress | GestureLongPressCallback? | - | 长按事件 | +| onTap | GestureTapCallback? | - | tab点击事件 | +| popUpButtonConfig | TBottomTabBarPopUpBtnConfig? | - | 弹窗配置 | +| selectedIcon | Widget? | - | 选中时图标 | +| selectTabTextStyle | TextStyle? | - | 文本已选择样式 basicType为text时必填 | +| tabText | String? | - | tab 文本 | +| unselectedIcon | Widget? | - | 未选中时图标 | +| unselectTabTextStyle | TextStyle? | - | 文本未选择样式 basicType为text时必填 | + +``` +``` + ### TBottomTabBarPopUpBtnConfig #### 默认构造方法 diff --git a/tdesign-component/example/assets/api/tabs_api.md b/tdesign-component/example/assets/api/tabs_api.md index d6029cae2..b55e32e45 100644 --- a/tdesign-component/example/assets/api/tabs_api.md +++ b/tdesign-component/example/assets/api/tabs_api.md @@ -1,17 +1,4 @@ ## API -### TTabBarView -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| children | List | - | 子widget列表 | -| controller | TabController? | - | 控制器 | -| isSlideSwitch | bool | false | 是否可以滑动切换 | -| key | | - | | - -``` -``` - ### TTabBar #### 默认构造方法 @@ -65,3 +52,16 @@ | size | TTabSize | TTabSize.small | 选项卡尺寸 | | text | String? | - | 文字内容 | | textMargin | EdgeInsetsGeometry? | - | 中间内容宽度 | + +``` +``` + +### TTabBarView +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| children | List | - | 子widget列表 | +| controller | TabController? | - | 控制器 | +| isSlideSwitch | bool | false | 是否可以滑动切换 | +| key | | - | | diff --git a/tdesign-component/example/assets/api/time-counter_api.md b/tdesign-component/example/assets/api/time-counter_api.md index 3b469863f..574d84ddb 100644 --- a/tdesign-component/example/assets/api/time-counter_api.md +++ b/tdesign-component/example/assets/api/time-counter_api.md @@ -1,4 +1,29 @@ ## API +### TTimeCounter +#### 简介 +计时组件 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| autoStart | bool | true | 是否自动开始倒计时 | +| content | dynamic | 'default' | 'default' / Widget Function(int time) / Widget | +| controller | TTimeCounterController? | - | 控制器,可控制开始/暂停/继续/重置 | +| direction | TTimeCounterDirection | TTimeCounterDirection.down | 计时方向,默认倒计时 | +| format | String | 'HH:mm:ss' | 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒(分隔符必须为长度为1的非空格的字符) | +| key | | - | | +| millisecond | bool | false | 是否开启毫秒级渲染 | +| onChange | Function(int time)? | - | 时间变化时触发回调 | +| onFinish | VoidCallback? | - | 计时结束时触发回调 | +| size | TTimeCounterSize | TTimeCounterSize.medium | 尺寸 | +| splitWithUnit | bool | false | 使用时间单位分割 | +| style | TTimeCounterStyle? | - | 自定义样式,有则优先用它,没有则根据size和theme选取 | +| theme | TTimeCounterTheme | TTimeCounterTheme.defaultTheme | 风格 | +| time | int | - | 必需;计时时长,单位毫秒 | + +``` +``` + ### TTimeCounterController #### 简介 倒计时组件控制器,可控制开始(`start()`)/暂停(`pause()`)/继续(`resume()`)/重置(`reset([int? time])`) @@ -34,28 +59,3 @@ | 名称 | 说明 | | --- | --- | | TTimeCounterStyle.generateStyle | 生成默认样式 | - -``` -``` - -### TTimeCounter -#### 简介 -计时组件 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| autoStart | bool | true | 是否自动开始倒计时 | -| content | dynamic | 'default' | 'default' / Widget Function(int time) / Widget | -| controller | TTimeCounterController? | - | 控制器,可控制开始/暂停/继续/重置 | -| direction | TTimeCounterDirection | TTimeCounterDirection.down | 计时方向,默认倒计时 | -| format | String | 'HH:mm:ss' | 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒(分隔符必须为长度为1的非空格的字符) | -| key | | - | | -| millisecond | bool | false | 是否开启毫秒级渲染 | -| onChange | Function(int time)? | - | 时间变化时触发回调 | -| onFinish | VoidCallback? | - | 计时结束时触发回调 | -| size | TTimeCounterSize | TTimeCounterSize.medium | 尺寸 | -| splitWithUnit | bool | false | 使用时间单位分割 | -| style | TTimeCounterStyle? | - | 自定义样式,有则优先用它,没有则根据size和theme选取 | -| theme | TTimeCounterTheme | TTimeCounterTheme.defaultTheme | 风格 | -| time | int | - | 必需;计时时长,单位毫秒 | diff --git a/tdesign-component/example/assets/api/tree-select_api.md b/tdesign-component/example/assets/api/tree-select_api.md index 9be631027..7ac293da2 100644 --- a/tdesign-component/example/assets/api/tree-select_api.md +++ b/tdesign-component/example/assets/api/tree-select_api.md @@ -1,19 +1,4 @@ ## API -### TSelectOption -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| children | List | const [] | 子选项 | -| columnWidth | double? | - | 自定义宽度,允许用户指定每个选项的宽度 | -| label | String | - | 标签 | -| maxLines | int | 1 | 最大显示行数 | -| multiple | bool | false | 当前子项支持多选 | -| value | dynamic | - | 值 | - -``` -``` - ### TTreeSelect #### 默认构造方法 @@ -27,3 +12,18 @@ | options | List | const [] | 展示的选项列表 | | outwardCornerRadius | double | 9 | 一级菜单选中项的外弯折圆角半径,默认为 9 | | style | TTreeSelectStyle | TTreeSelectStyle.normal | 一级菜单样式 | + +``` +``` + +### TSelectOption +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| children | List | const [] | 子选项 | +| columnWidth | double? | - | 自定义宽度,允许用户指定每个选项的宽度 | +| label | String | - | 标签 | +| maxLines | int | 1 | 最大显示行数 | +| multiple | bool | false | 当前子项支持多选 | +| value | dynamic | - | 值 | diff --git a/tdesign-site/src/action-sheet/README.md b/tdesign-site/src/action-sheet/README.md index 7ed941f9a..79a1a954c 100644 --- a/tdesign-site/src/action-sheet/README.md +++ b/tdesign-site/src/action-sheet/README.md @@ -887,6 +887,25 @@ Widget _buildIconListLeftActionSheet(BuildContext context) { ## API +### TActionSheetItem +#### 简介 +动作面板项目 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| badge | TBadge? | - | 角标 | +| description | String? | - | 描述信息 | +| disabled | bool | false | 是否禁用 | +| group | String? | - | 分组,用于带描述多行滚动宫格 | +| icon | Widget? | - | 图标 | +| iconSize | double? | - | 图标大小 | +| label | String | - | 标题 | +| textStyle | TextStyle? | - | 标题样式 | + +``` +``` + ### TActionSheet #### 简介 动作面板 @@ -924,24 +943,5 @@ Widget _buildIconListLeftActionSheet(BuildContext context) { | showGroupActionSheet | | required BuildContext context, required List items, TActionSheetAlign align, String? cancelText, bool showCancel, TActionSheetItemCallback? onSelected, bool showOverlay, bool closeOnOverlayClick, double itemHeight, double itemMinWidth, VoidCallback? onCancel, VoidCallback? onClose, bool useSafeArea, | 显示分组类型面板 | | showListActionSheet | | required BuildContext context, required List items, TActionSheetAlign align, String? cancelText, bool showCancel, VoidCallback? onCancel, TActionSheetItemCallback? onSelected, bool showOverlay, bool closeOnOverlayClick, VoidCallback? onClose, bool useSafeArea, | 显示列表类型面板 | -``` -``` - -### TActionSheetItem -#### 简介 -动作面板项目 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| badge | TBadge? | - | 角标 | -| description | String? | - | 描述信息 | -| disabled | bool | false | 是否禁用 | -| group | String? | - | 分组,用于带描述多行滚动宫格 | -| icon | Widget? | - | 图标 | -| iconSize | double? | - | 图标大小 | -| label | String | - | 标题 | -| textStyle | TextStyle? | - | 标题样式 | - \ No newline at end of file diff --git a/tdesign-site/src/button/README.md b/tdesign-site/src/button/README.md index cf08aad1a..b21519c3a 100644 --- a/tdesign-site/src/button/README.md +++ b/tdesign-site/src/button/README.md @@ -738,31 +738,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ## API -### TButtonStyle -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| backgroundColor | Color? | - | 背景颜色 | -| frameColor | Color? | - | 边框颜色 | -| frameWidth | double? | - | 边框宽度 | -| gradient | Gradient? | - | 渐变背景色 | -| radius | BorderRadiusGeometry? | - | 自定义圆角 | -| textColor | Color? | - | 文字颜色 | - - -#### 工厂构造方法 - -| 名称 | 说明 | -| --- | --- | -| TButtonStyle.generateFillStyleByTheme | 生成不同主题的填充按钮样式 | -| TButtonStyle.generateGhostStyleByTheme | 生成不同主题的幽灵按钮样式 | -| TButtonStyle.generateOutlineStyleByTheme | 生成不同主题的描边按钮样式 | -| TButtonStyle.generateTextStyleByTheme | 生成不同主题的文本按钮样式 | - -``` -``` - ### TButton #### 默认构造方法 @@ -794,5 +769,30 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | type | TButtonType | TButtonType.fill | 类型:填充,描边,文字 | | width | double? | - | 自定义宽度 | +``` +``` + +### TButtonStyle +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| backgroundColor | Color? | - | 背景颜色 | +| frameColor | Color? | - | 边框颜色 | +| frameWidth | double? | - | 边框宽度 | +| gradient | Gradient? | - | 渐变背景色 | +| radius | BorderRadiusGeometry? | - | 自定义圆角 | +| textColor | Color? | - | 文字颜色 | + + +#### 工厂构造方法 + +| 名称 | 说明 | +| --- | --- | +| TButtonStyle.generateFillStyleByTheme | 生成不同主题的填充按钮样式 | +| TButtonStyle.generateGhostStyleByTheme | 生成不同主题的幽灵按钮样式 | +| TButtonStyle.generateOutlineStyleByTheme | 生成不同主题的描边按钮样式 | +| TButtonStyle.generateTextStyleByTheme | 生成不同主题的文本按钮样式 | + \ No newline at end of file diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md index c1b3d87f3..43875fc92 100644 --- a/tdesign-site/src/calendar/README.md +++ b/tdesign-site/src/calendar/README.md @@ -27,546 +27,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 Widget _buildSimple(BuildContext context) {
-  final size = MediaQuery.of(context).size;
-  final selected = ValueNotifier>(
-      [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]);
-  // 刷新触发器:所有示例 onConfirm 后 +1,驱动 builder 重建以更新 note
-  final refreshTrigger = ValueNotifier(0);
-
-  // 时间选择器 items(时 0-23、分 0-59),一次性构造避免重复创建
-  final timeItems = TPickerColumns([
-    [for (int i = 0; i < 24; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}时', value: i)],
-    [for (int i = 0; i < 60; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}分', value: i)],
-  ]);
-  final now = DateTime.now();
-  // 单个选择日历和时间 - 时分
-  final pickedTime = ValueNotifier>([now.hour, now.minute]);
-  // 区间选择日历和时间 - [[开始时, 开始分], [结束时, 结束分]]
-  final pickedRangeTime = ValueNotifier>>([
-    [now.hour, now.minute],
-    [now.hour, now.minute],
-  ]);
-  final rangeTimeTab = ValueNotifier(0);
-
-  // 单个选择日历 - 已选日期(默认无选中)
-  var singleSelected = [];
-  // 单个选择日历 - 天气面板是否展开(默认收起,点击日期/已有选中时展开)
-  final singleWeatherExpanded = ValueNotifier(false);
-  // 多个选择日历 - 已选日期(闭包捕获,在 builder 内通过 refreshTrigger 触发更新)
-  var multipleDates = [];
-  // 区间选择日历 - 已选区间
-  var rangeDates = [
-    DateTime.now().millisecondsSinceEpoch,
-    DateTime.now().add(const Duration(days: 6)).millisecondsSinceEpoch,
-  ];
-  // 区间选择日历和时间 - 已选区间(带时分)
-  var rangeTimeDates = [];
-
-  return ValueListenableBuilder(
-    valueListenable: refreshTrigger,
-    builder: (context, _, child) {
-      final date = DateTime.fromMillisecondsSinceEpoch(selected.value[0]);
-      // 多个选择 note
-      final multipleNote = multipleDates.isEmpty
-          ? '--'
-          : '已选 ${multipleDates.length} 天';
-      // 区间选择 note
-      String fmtRange(int ms) {
-        final d = DateTime.fromMillisecondsSinceEpoch(ms);
-        return '${d.month}/${d.day}';
-      }
-      // 区间选择日历和时间 note
-      String fmtRangeTime(int ms) {
-        final d = DateTime.fromMillisecondsSinceEpoch(ms);
-        return '${d.month}/${d.day} ${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}';
-      }
-      final rangeTimeNote = rangeTimeDates.length >= 2
-          ? '${fmtRangeTime(rangeTimeDates.first)} ~ ${fmtRangeTime(rangeTimeDates.last)}'
-          : '--';
-
-      return TCellGroup(
-        cells: [
-          TCell(
-            title: '单个选择日历',
-            arrow: true,
-            note: singleSelected.isEmpty
-                ? '--'
-                : () {
-                    final d = DateTime.fromMillisecondsSinceEpoch(singleSelected.first);
-                    return '${d.year}-${d.month}-${d.day}';
-                  }(),
-            onClick: (cell) {
-              // 打开时:若已有选中值则默认展开天气,否则收起
-              singleWeatherExpanded.value = singleSelected.isNotEmpty;
-              TCalendarPopup(
-                context,
-                visible: true,
-                onConfirm: (value) {
-                  singleSelected = value;
-                  refreshTrigger.value++;
-                },
-                onClose: () {},
-                child: TCalendar(
-                  title: '请选择日期',
-                  value: singleSelected,
-                  height: size.height * 0.6 + 176,
-                  bottomExpandedListenable: singleWeatherExpanded,
-                  onCellClick: (value, type, tdate) {
-                    // 点击日期时展开天气面板
-                    singleWeatherExpanded.value = true;
-                  },
-                  onCellLongPress: (value, type, tdate) {
-                    print('onCellLongPress: $value');
-                  },
-                  onHeaderClick: (index, week) {
-                    print('onHeaderClick: $week');
-                  },
-                  onChange: (value) {
-                    print('onChange: $value');
-                  },
-                  bottom: (context, selectedDates) {
-                    // 随机天气数据
-                    final weathers = ['☀️ 晴', '⛅ 多云', '🌧️ 小雨', '⛈️ 雷阵雨', '❄️ 小雪', '🌫️ 雾'];
-                    final windDirs = ['北风', '南风', '东风', '西风', '微风'];
-                    final w = weathers[DateTime.now().millisecond % weathers.length];
-                    final temp = -5 + (DateTime.now().millisecond % 30);
-                    final hum = 30 + (DateTime.now().millisecond % 50);
-                    final wind = windDirs[DateTime.now().millisecond % windDirs.length];
-                    final windLv = 1 + (DateTime.now().millisecond % 5);
-                    final d = selectedDates.isEmpty
-                        ? DateTime.now()
-                        : DateTime.fromMillisecondsSinceEpoch(selectedDates.first);
-                    return Container(
-                      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
-                      decoration: BoxDecoration(
-                        color: Theme.of(context).colorScheme.surface,
-                        boxShadow: const [
-                          BoxShadow(
-                            color: Color.fromRGBO(0, 0, 0, 0.04),
-                            blurRadius: 12,
-                            offset: Offset(0, -2),
-                          ),
-                        ],
-                      ),
-                      child: Row(
-                        children: [
-                          Column(
-                            crossAxisAlignment: CrossAxisAlignment.start,
-                            children: [
-                              Text('${d.year}-${d.month}-${d.day}', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
-                              const SizedBox(height: 4),
-                              Text(w, style: const TextStyle(fontSize: 22)),
-                            ],
-                          ),
-                          const SizedBox(width: 24),
-                          Expanded(
-                            child: Column(
-                              crossAxisAlignment: CrossAxisAlignment.start,
-                              children: [
-                                Row(children: [const Icon(Icons.thermostat, size: 14), const SizedBox(width: 4), Text('$temp°C')]),
-                                const SizedBox(height: 4),
-                                Row(children: [const Icon(Icons.water_drop, size: 14), const SizedBox(width: 4), Text('$hum%')]),
-                                const SizedBox(height: 4),
-                                Row(children: [const Icon(Icons.air, size: 14), const SizedBox(width: 4), Text('$wind $windLv 级')]),
-                              ],
-                            ),
-                          ),
-                        ],
-                      ),
-                    );
-                  },
-                ),
-              );
-            },
-          ),
-          TCell(
-            title: '多个选择日历',
-            arrow: true,
-            note: multipleNote,
-            onClick: (cell) {
-              TCalendarPopup(
-                context,
-                visible: true,
-                onConfirm: (value) {
-                  multipleDates = value;
-                  refreshTrigger.value++;
-                },
-                child: TCalendar(
-                  title: '请选择日期',
-                  type: CalendarType.multiple,
-                  value: multipleDates.isEmpty
-                      ? [DateTime.now().millisecondsSinceEpoch]
-                      : multipleDates,
-                  height: size.height * 0.6 + 176,
-                  bottom: (context, selectedDates) {
-                    final dates = selectedDates
-                        .map(DateTime.fromMillisecondsSinceEpoch)
-                        .toList()
-                      ..sort();
-                    return Container(
-                      padding: const EdgeInsets.symmetric(
-                          horizontal: 16, vertical: 12),
-                      decoration: BoxDecoration(
-                        color: Theme.of(context).colorScheme.surface,
-                        boxShadow: const [
-                          BoxShadow(
-                            color: Color.fromRGBO(0, 0, 0, 0.04),
-                            blurRadius: 12,
-                            offset: Offset(0, -2),
-                          ),
-                        ],
-                      ),
-                      child: Column(
-                        crossAxisAlignment: CrossAxisAlignment.start,
-                        mainAxisSize: MainAxisSize.min,
-                        children: [
-                          Text(
-                            '已选择 ${dates.length} 天',
-                            style: const TextStyle(
-                                fontSize: 14, fontWeight: FontWeight.w500),
-                          ),
-                          const SizedBox(height: 6),
-                          Wrap(
-                            spacing: 8,
-                            runSpacing: 6,
-                            children: dates
-                                .map((d) => Container(
-                                      padding: const EdgeInsets.symmetric(
-                                          horizontal: 8, vertical: 4),
-                                      decoration: BoxDecoration(
-                                        color: TTheme.of(context).brandColor1,
-                                        borderRadius:
-                                            BorderRadius.circular(4),
-                                      ),
-                                      child: Text(
-                                        '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}',
-                                        style: TextStyle(
-                                            fontSize: 12,
-                                            color: TTheme.of(context)
-                                                .brandColor7),
-                                      ),
-                                    ))
-                                .toList(),
-                          ),
-                        ],
-                      ),
-                    );
-                  },
-                ),
-              );
-            },
-          ),
-          TCell(
-            title: '区间选择日历',
-            arrow: true,
-            note: rangeDates.length >= 2
-                ? '${fmtRange(rangeDates.first)} ~ ${fmtRange(rangeDates[1])}'
-                : '--',
-            onClick: (cell) {
-              TCalendarPopup(
-                context,
-                visible: true,
-                onConfirm: (value) {
-                  rangeDates = value;
-                  refreshTrigger.value++;
-                },
-                child: TCalendar(
-                  title: '请选择日期区间',
-                  type: CalendarType.range,
-                  value: rangeDates,
-                  height: size.height * 0.6 + 176,
-                  bottom: (context, selectedDates) {
-                    String formatDate(int ms) {
-                      final d = DateTime.fromMillisecondsSinceEpoch(ms);
-                      return '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
-                    }
-
-                    final hasStart = selectedDates.isNotEmpty;
-                    final hasEnd = selectedDates.length >= 2;
-                    final days = hasEnd
-                        ? ((selectedDates[1] - selectedDates[0]) /
-                                    (24 * 60 * 60 * 1000))
-                                .round() +
-                            1
-                        : (hasStart ? 1 : 0);
-
-                    Widget buildSegment(String label, String? value) {
-                      return Column(
-                        crossAxisAlignment: CrossAxisAlignment.start,
-                        children: [
-                          Text(
-                            label,
-                            style: TextStyle(
-                                fontSize: 12,
-                                color: TTheme.of(context).fontGyColor3),
-                          ),
-                          const SizedBox(height: 2),
-                          Text(
-                            value ?? '--',
-                            style: TextStyle(
-                              fontSize: 15,
-                              fontWeight: FontWeight.w500,
-                              color: value != null
-                                  ? TTheme.of(context).fontGyColor1
-                                  : TTheme.of(context).fontGyColor3,
-                            ),
-                          ),
-                        ],
-                      );
-                    }
-
-                    return Container(
-                      padding: const EdgeInsets.symmetric(
-                          horizontal: 16, vertical: 12),
-                      decoration: BoxDecoration(
-                        color: Theme.of(context).colorScheme.surface,
-                        boxShadow: const [
-                          BoxShadow(
-                            color: Color.fromRGBO(0, 0, 0, 0.04),
-                            blurRadius: 12,
-                            offset: Offset(0, -2),
-                          ),
-                        ],
-                      ),
-                      child: Row(
-                        children: [
-                          Expanded(
-                            child: buildSegment('开始',
-                                hasStart ? formatDate(selectedDates[0]) : null),
-                          ),
-                          Icon(Icons.arrow_forward,
-                              size: 16,
-                              color: TTheme.of(context).fontGyColor3),
-                          const SizedBox(width: 12),
-                          Expanded(
-                            child: buildSegment('结束',
-                                hasEnd ? formatDate(selectedDates[1]) : null),
-                          ),
-                          if (days > 0)
-                            Container(
-                              padding: const EdgeInsets.symmetric(
-                                  horizontal: 8, vertical: 4),
-                              decoration: BoxDecoration(
-                                color: TTheme.of(context).brandColor1,
-                                borderRadius: BorderRadius.circular(4),
-                              ),
-                              child: Text(
-                                '共 $days 天',
-                                style: TextStyle(
-                                    fontSize: 12,
-                                    color: TTheme.of(context).brandColor7),
-                              ),
-                            ),
-                        ],
-                      ),
-                    );
-                  },
-                ),
-              );
-            },
-          ),
-          TCell(
-            title: '单个选择日历和时间',
-            arrow: true,
-            note:
-                '${date.year}-${date.month}-${date.day} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}',
-            onClick: (cell) {
-              TCalendarPopup(
-                context,
-                visible: true,
-                onConfirm: (dates) {
-                  // 将 Picker 选中的时分合并到日期时间戳
-                  final merged = dates.map((ms) {
-                    final d = DateTime.fromMillisecondsSinceEpoch(ms);
-                    return DateTime(
-                      d.year,
-                      d.month,
-                      d.day,
-                      pickedTime.value[0],
-                      pickedTime.value[1],
-                    ).millisecondsSinceEpoch;
-                  }).toList();
-                  print('onConfirm:$merged');
-                  selected.value = merged;
-                  refreshTrigger.value++;
-                },
-                onClose: () {
-                  print('onClose');
-                },
-                child: TCalendar(
-                  title: '请选择日期和时间',
-                  value: selected.value,
-                  height: size.height * 0.92,
-                  bottom: (context, selectedDates) {
-                    return Container(
-                      decoration: BoxDecoration(
-                        color: Theme.of(context).colorScheme.surface,
-                        boxShadow: const [
-                          BoxShadow(
-                            color: Color.fromRGBO(0, 0, 0, 0.04),
-                            blurRadius: 12,
-                            offset: Offset(0, -2),
-                          ),
-                        ],
-                      ),
-                      child: TPicker(
-                        items: timeItems,
-                        initialValue: pickedTime.value,
-                        height: 180,
-                        itemCount: 5,
-                        title: '选择时间',
-                        onChange: (v) =>
-                            pickedTime.value = List.from(v.values),
-                      ),
-                    );
-                  },
-                  onCellClick: (value, type, tdate) {
-                    print('onCellClick: $value');
-                  },
-                  onCellLongPress: (value, type, tdate) {
-                    print('onCellLongPress: $value');
-                  },
-                  onHeaderClick: (index, week) {
-                    print('onHeaderClick: $week');
-                  },
-                  onChange: (value) {
-                    print('onChange: $value');
-                  },
-                ),
-              );
-            },
-          ),
-          TCell(
-            title: '区间选择日历和时间',
-            arrow: true,
-            note: rangeTimeNote,
-            onClick: (cell) {
-              TCalendarPopup(
-                context,
-                visible: true,
-                onConfirm: (value) {
-                  // 把开始/结束时分合并到对应日期
-                  final rt = pickedRangeTime.value;
-                  final merged = [
-                    for (var i = 0; i < value.length; i++)
-                      DateTime.fromMillisecondsSinceEpoch(value[i])
-                          .copyWith(hour: rt[i][0], minute: rt[i][1])
-                          .millisecondsSinceEpoch,
-                  ];
-                  print('onConfirm: $merged');
-                  rangeTimeDates = merged;
-                  refreshTrigger.value++;
-                },
-                onClose: () {
-                  print('onClose');
-                },
-                child: TCalendar(
-                  title: '请选择日期和时间区间',
-                  height: size.height * 0.92,
-                  type: CalendarType.range,
-                  value: [
-                    DateTime.now().millisecondsSinceEpoch,
-                    DateTime.now()
-                        .add(const Duration(days: 3))
-                        .millisecondsSinceEpoch,
-                  ],
-                  bottom: (context, selectedDates) {
-                    return DefaultTabController(
-                      length: 2,
-                      child: Container(
-                        decoration: BoxDecoration(
-                          color: Theme.of(context).colorScheme.surface,
-                          boxShadow: const [
-                            BoxShadow(
-                              color: Color.fromRGBO(0, 0, 0, 0.04),
-                              blurRadius: 12,
-                              offset: Offset(0, -2),
-                            ),
-                          ],
-                        ),
-                        child: Column(
-                          mainAxisSize: MainAxisSize.min,
-                          children: [
-                            TTabBar(
-                              height: 40,
-                              showIndicator: true,
-                              tabs: const [
-                                TTab(text: '开始时间'),
-                                TTab(text: '结束时间'),
-                              ],
-                              onTap: (i) => rangeTimeTab.value = i,
-                            ),
-                            ValueListenableBuilder(
-                              valueListenable: rangeTimeTab,
-                              builder: (context, tab, _) => TPicker(
-                                // 切换 tab 时重建,复位到对应时分
-                                key: ValueKey(tab),
-                                items: timeItems,
-                                initialValue: pickedRangeTime.value[tab],
-                                height: 180,
-                                itemCount: 5,
-                                onChange: (v) {
-                                  final next = [...pickedRangeTime.value];
-                                  next[tab] = List.from(v.values);
-                                  pickedRangeTime.value = next;
-                                },
-                              ),
-                            ),
-                          ],
-                        ),
-                      ),
-                    );
-                  },
-                  onChange: (value) {
-                    print('onChange: $value');
-                  },
-                ),
-              );
-            },
-          ),
-          TCell(
-            title: '添加锚点',
-            arrow: true,
-            note: '${date.year}-${date.month}-${date.day}',
-            onClick: (cell) {
-              TCalendarPopup(
-                context,
-                visible: true,
-                onConfirm: (dates) {
-                  print('onConfirm:$dates');
-                  selected.value = dates;
-                  refreshTrigger.value++;
-                },
-                onClose: () {
-                  print('onClose');
-                },
-                child: TCalendar(
-                  title: '请选择日期',
-                  minDate: DateTime(2022, 1, 1).millisecondsSinceEpoch,
-                  maxDate: DateTime(2028, 2, 15).millisecondsSinceEpoch,
-                  anchorDate: DateTime(2026, 5),
-                  value: selected.value,
-                  height: size.height * 0.6 + 176,
-                  onCellClick: (value, type, tdate) {
-                    print('onCellClick: $value');
-                  },
-                  onCellLongPress: (value, type, tdate) {
-                    print('onCellLongPress: $value');
-                  },
-                  onHeaderClick: (index, week) {
-                    print('onHeaderClick: $week');
-                  },
-                  onChange: (value) {
-                    print('onChange: $value');
-                  },
-                ),
-              );
-            },
-          ),
-        ],
-      );
-    },
-  );
+  return const _SimpleDemo();
 }
@@ -1783,34 +1244,6 @@ Widget _buildLunar(BuildContext context) { ## API -### TCalendarStyle -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| cellDecoration | BoxDecoration? | - | 日期decoration | -| cellPrefixStyle | TextStyle? | - | 日期前面的字符串的样式 | -| cellStyle | TextStyle? | - | 日期样式 | -| cellSuffixStyle | TextStyle? | - | 日期后面的字符串的样式 | -| centreColor | Color? | - | 日期范围内背景样式 | -| decoration | | - | | -| monthTitleStyle | TextStyle? | - | body区域 年月文字样式 | -| titleCloseColor | Color? | - | header区域 关闭图标的颜色 | -| titleMaxLine | int? | - | header区域 [TCalendar.title]的行数 | -| titleStyle | TextStyle? | - | header区域 [TCalendar.title]的样式 | -| weekdayStyle | TextStyle? | - | header区域 周 文字样式 | - - -#### 工厂构造方法 - -| 名称 | 说明 | -| --- | --- | -| TCalendarStyle.cellStyle | 日期样式 | -| TCalendarStyle.generateStyle | 生成默认样式 | - -``` -``` - ### TCalendar #### 默认构造方法 @@ -1818,9 +1251,8 @@ Widget _buildLunar(BuildContext context) { | --- | --- | --- | --- | | anchorDate | DateTime? | - | 锚点日期 | | animateTo | bool? | false | 动画滚动到指定位置 | -| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,位于日历主体浮层上方 | -| bottomExpanded | bool | true | bottom 区域是否展开,默认 true | -| bottomExpandedListenable | ValueListenable? | - | bottom 区域是否展开(响应式版本,优先级高于 [bottomExpanded])。 | +| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,位于日历主体浮层上方。 | +| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。 | | cellHeight | double? | 60 | 日期高度 | | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | | dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 | @@ -1851,10 +1283,6 @@ Widget _buildLunar(BuildContext context) { ``` ``` -### TCalendarDataSource -``` -``` - ### TCalendarPopup #### 默认构造方法 @@ -1873,6 +1301,38 @@ Widget _buildLunar(BuildContext context) { ``` ``` +### TCalendarStyle +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| cellDecoration | BoxDecoration? | - | 日期decoration | +| cellPrefixStyle | TextStyle? | - | 日期前面的字符串的样式 | +| cellStyle | TextStyle? | - | 日期样式 | +| cellSuffixStyle | TextStyle? | - | 日期后面的字符串的样式 | +| centreColor | Color? | - | 日期范围内背景样式 | +| decoration | | - | | +| monthTitleStyle | TextStyle? | - | body区域 年月文字样式 | +| titleCloseColor | Color? | - | header区域 关闭图标的颜色 | +| titleMaxLine | int? | - | header区域 [TCalendar.title]的行数 | +| titleStyle | TextStyle? | - | header区域 [TCalendar.title]的样式 | +| weekdayStyle | TextStyle? | - | header区域 周 文字样式 | + + +#### 工厂构造方法 + +| 名称 | 说明 | +| --- | --- | +| TCalendarStyle.cellStyle | 日期样式 | +| TCalendarStyle.generateStyle | 生成默认样式 | + +``` +``` + +### TCalendarDataSource +``` +``` + ### TLunarInfo #### 默认构造方法 diff --git a/tdesign-site/src/cell/README.md b/tdesign-site/src/cell/README.md index 1dbddbd7a..163f51ad7 100644 --- a/tdesign-site/src/cell/README.md +++ b/tdesign-site/src/cell/README.md @@ -158,6 +158,45 @@ Widget _buildCard(BuildContext context) { ## API +### TCell +#### 简介 +单元格组件 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| align | TCellAlign? | TCellAlign.middle | 内容的对齐方式,默认居中对齐。可选项:top/middle/bottom | +| arrow | bool? | false | 是否显示右侧箭头 | +| bordered | bool? | true | 是否显示下边框,仅在TCellGroup组件下起作用 | +| description | String? | - | 下方内容描述文字 | +| descriptionWidget | Widget? | - | 下方内容描述组件 | +| disabled | bool? | false | 禁用 | +| height | double? | - | 高度 | +| hover | bool? | true | 是否开启点击反馈 | +| image | ImageProvider? | - | 主图 | +| imageCircle | double? | 50 | 主图圆角,默认50(圆形) | +| imageSize | double? | - | 主图尺寸 | +| imageWidget | Widget? | - | 主图组件 | +| key | | - | | +| leftIcon | IconData? | - | 左侧图标,出现在单元格标题的左侧 | +| leftIconWidget | Widget? | - | 左侧图标组件 | +| note | String? | - | 和标题同行的说明文字 | +| noteMaxLine | int | 1 | 说明文字组件 最大行数 | +| noteMaxWidth | double? | - | 说明文字组件 最大宽度,超过部分显示省略号,防止文字溢出 | +| noteWidget | Widget? | - | 说明文字组件 | +| onClick | TCellClick? | - | 点击事件 | +| onLongPress | TCellClick? | - | 长按事件 | +| required | bool? | false | 是否显示表单必填星号 | +| rightIcon | IconData? | - | 最右侧图标 | +| rightIconWidget | Widget? | - | 最右侧图标组件 | +| showBottomBorder | bool? | false | 是否显示下边框(建议TCellGroup组件下false,避免与bordered重叠) | +| style | TCellStyle? | - | 自定义样式 | +| title | String? | - | 标题 | +| titleWidget | Widget? | - | 标题组件 | + +``` +``` + ### TCellGroup #### 简介 单元格组组件 @@ -212,44 +251,5 @@ Widget _buildCard(BuildContext context) { | --- | --- | | TCellStyle.cellStyle | 生成单元格默认样式 | -``` -``` - -### TCell -#### 简介 -单元格组件 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| align | TCellAlign? | TCellAlign.middle | 内容的对齐方式,默认居中对齐。可选项:top/middle/bottom | -| arrow | bool? | false | 是否显示右侧箭头 | -| bordered | bool? | true | 是否显示下边框,仅在TCellGroup组件下起作用 | -| description | String? | - | 下方内容描述文字 | -| descriptionWidget | Widget? | - | 下方内容描述组件 | -| disabled | bool? | false | 禁用 | -| height | double? | - | 高度 | -| hover | bool? | true | 是否开启点击反馈 | -| image | ImageProvider? | - | 主图 | -| imageCircle | double? | 50 | 主图圆角,默认50(圆形) | -| imageSize | double? | - | 主图尺寸 | -| imageWidget | Widget? | - | 主图组件 | -| key | | - | | -| leftIcon | IconData? | - | 左侧图标,出现在单元格标题的左侧 | -| leftIconWidget | Widget? | - | 左侧图标组件 | -| note | String? | - | 和标题同行的说明文字 | -| noteMaxLine | int | 1 | 说明文字组件 最大行数 | -| noteMaxWidth | double? | - | 说明文字组件 最大宽度,超过部分显示省略号,防止文字溢出 | -| noteWidget | Widget? | - | 说明文字组件 | -| onClick | TCellClick? | - | 点击事件 | -| onLongPress | TCellClick? | - | 长按事件 | -| required | bool? | false | 是否显示表单必填星号 | -| rightIcon | IconData? | - | 最右侧图标 | -| rightIconWidget | Widget? | - | 最右侧图标组件 | -| showBottomBorder | bool? | false | 是否显示下边框(建议TCellGroup组件下false,避免与bordered重叠) | -| style | TCellStyle? | - | 自定义样式 | -| title | String? | - | 标题 | -| titleWidget | Widget? | - | 标题组件 | - \ No newline at end of file diff --git a/tdesign-site/src/checkbox/README.md b/tdesign-site/src/checkbox/README.md index 3ee440e7c..3fde5c091 100644 --- a/tdesign-site/src/checkbox/README.md +++ b/tdesign-site/src/checkbox/README.md @@ -426,28 +426,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ## API -### TCheckboxGroup -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| checkedIds | List? | - | 勾选的CheckBox id列表 | -| child | | - | | -| contentDirection | TContentDirection? | - | 文字相对icon的方位 | -| controller | TCheckboxGroupController? | - | 可以通过控制器操作勾选状态 | -| customContentBuilder | ContentBuilder? | - | CheckBox完全自定义内容 | -| customIconBuilder | IconBuilder? | - | 自定义选择icon的样式 | -| key | | - | | -| maxChecked | int? | - | 最多可以勾选多少 | -| onChangeGroup | OnGroupChange? | - | 状态变化监听器 | -| onOverloadChecked | VoidCallback? | - | 超过最大可勾选的个数 | -| spacing | double? | - | CheckBoxicon和文字的距离 | -| style | TCheckboxStyle? | - | CheckBox复选框样式:圆形或方形 | -| titleMaxLine | int? | - | CheckBox标题的行数 | - -``` -``` - ### TCheckbox #### 默认构造方法 @@ -481,5 +459,27 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | titleFont | Font? | - | 标题字体大小 | | titleMaxLine | int? | - | 标题的行数 | +``` +``` + +### TCheckboxGroup +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| checkedIds | List? | - | 勾选的CheckBox id列表 | +| child | | - | | +| contentDirection | TContentDirection? | - | 文字相对icon的方位 | +| controller | TCheckboxGroupController? | - | 可以通过控制器操作勾选状态 | +| customContentBuilder | ContentBuilder? | - | CheckBox完全自定义内容 | +| customIconBuilder | IconBuilder? | - | 自定义选择icon的样式 | +| key | | - | | +| maxChecked | int? | - | 最多可以勾选多少 | +| onChangeGroup | OnGroupChange? | - | 状态变化监听器 | +| onOverloadChecked | VoidCallback? | - | 超过最大可勾选的个数 | +| spacing | double? | - | CheckBoxicon和文字的距离 | +| style | TCheckboxStyle? | - | CheckBox复选框样式:圆形或方形 | +| titleMaxLine | int? | - | CheckBox标题的行数 | + \ No newline at end of file diff --git a/tdesign-site/src/dialog/README.md b/tdesign-site/src/dialog/README.md index 4eaa73dcf..287b1a962 100644 --- a/tdesign-site/src/dialog/README.md +++ b/tdesign-site/src/dialog/README.md @@ -703,28 +703,39 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ## API -### TImageDialog +### TAlertDialog #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | backgroundColor | Color? | - | 背景颜色 | +| buttonStyle | | TDialogButtonStyle.normal | | | buttonWidget | Widget? | - | 自定义按钮 | | content | String? | - | 内容 | | contentColor | Color? | - | 内容颜色 | +| contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 | | contentWidget | Widget? | - | 内容Widget | -| image | Image | - | 图片 | -| imagePosition | TDialogImagePosition? | TDialogImagePosition.top | 图片位置 | | key | | - | | | leftBtn | TDialogButtonOptions? | - | 左侧按钮配置 | -| padding | EdgeInsets? | - | 内容内边距 | +| leftBtnAction | Function()? | - | 左侧按钮默认点击 | +| padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 | | radius | double | 12.0 | 圆角 | | rightBtn | TDialogButtonOptions? | - | 右侧按钮配置 | +| rightBtnAction | Function()? | - | 右侧按钮默认点击 | | showCloseButton | bool? | - | 显示右上角关闭按钮 | | title | String? | - | 标题 | | titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | | titleColor | Color? | - | 标题颜色 | + +#### 工厂构造方法 + +| 名称 | 说明 | +| --- | --- | +| TAlertDialog.vertical | 纵向按钮排列的对话框 + + [buttons]参数是必须的,纵向按钮默认样式都是[TButtonTheme.primary] | + ``` ``` @@ -756,6 +767,24 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ``` ``` +### TDialogButtonOptions +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| action | Function()? | - | 点击操作 | +| fontWeight | FontWeight? | - | 字体粗细 | +| height | double? | - | 按钮高度 | +| style | TButtonStyle? | - | 按钮样式 | +| theme | TButtonTheme? | - | 按钮类型 | +| title | String | - | 标题内容 | +| titleColor | Color? | - | 标题颜色 | +| titleSize | double? | - | 字体大小 | +| type | TButtonType? | - | 按钮类型 | + +``` +``` + ### TDialogScaffold #### 默认构造方法 @@ -858,57 +887,28 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ``` ``` -### TDialogButtonOptions -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| action | Function()? | - | 点击操作 | -| fontWeight | FontWeight? | - | 字体粗细 | -| height | double? | - | 按钮高度 | -| style | TButtonStyle? | - | 按钮样式 | -| theme | TButtonTheme? | - | 按钮类型 | -| title | String | - | 标题内容 | -| titleColor | Color? | - | 标题颜色 | -| titleSize | double? | - | 字体大小 | -| type | TButtonType? | - | 按钮类型 | - -``` -``` - -### TAlertDialog +### TImageDialog #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | backgroundColor | Color? | - | 背景颜色 | -| buttonStyle | | TDialogButtonStyle.normal | | | buttonWidget | Widget? | - | 自定义按钮 | | content | String? | - | 内容 | | contentColor | Color? | - | 内容颜色 | -| contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 | | contentWidget | Widget? | - | 内容Widget | +| image | Image | - | 图片 | +| imagePosition | TDialogImagePosition? | TDialogImagePosition.top | 图片位置 | | key | | - | | | leftBtn | TDialogButtonOptions? | - | 左侧按钮配置 | -| leftBtnAction | Function()? | - | 左侧按钮默认点击 | -| padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 | +| padding | EdgeInsets? | - | 内容内边距 | | radius | double | 12.0 | 圆角 | | rightBtn | TDialogButtonOptions? | - | 右侧按钮配置 | -| rightBtnAction | Function()? | - | 右侧按钮默认点击 | | showCloseButton | bool? | - | 显示右上角关闭按钮 | | title | String? | - | 标题 | | titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | | titleColor | Color? | - | 标题颜色 | - -#### 工厂构造方法 - -| 名称 | 说明 | -| --- | --- | -| TAlertDialog.vertical | 纵向按钮排列的对话框 - - [buttons]参数是必须的,纵向按钮默认样式都是[TButtonTheme.primary] | - ``` ``` diff --git a/tdesign-site/src/drawer/README.md b/tdesign-site/src/drawer/README.md index a203ebfba..1e4ad50dd 100644 --- a/tdesign-site/src/drawer/README.md +++ b/tdesign-site/src/drawer/README.md @@ -285,71 +285,71 @@ Widget _buildBottomSimple(BuildContext context) { ## API -### TDrawerWidget +### TDrawer #### 简介 -抽屉内容组件 - 可用于 Scaffold 中的 drawer 属性 +抽屉组件 #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | backgroundColor | Color? | - | 组件背景颜色 | | bordered | bool? | true | 是否显示边框 | +| closeOnOverlayClick | bool? | true | 点击蒙层时是否关闭抽屉 | | contentWidget | Widget? | - | 自定义内容,优先级高于[items]/[footer]/[title] | +| context | BuildContext | context | 上下文 | +| drawerTop | double? | - | 距离顶部的距离 | | footer | Widget? | - | 抽屉的底部 | | hover | bool? | true | 是否开启点击反馈 | | isShowLastBordered | bool? | true | 是否显示最后一行分割线 | | items | List? | - | 抽屉里的列表项 | -| key | | - | | +| onClose | VoidCallback? | - | 关闭时触发 | | onItemClick | TDrawerItemClickCallback? | - | 点击抽屉里的列表项触发 | +| placement | TDrawerPlacement? | TDrawerPlacement.right | 抽屉方向 | +| showOverlay | bool? | true | 是否显示遮罩层 | | style | TCellStyle? | - | 列表自定义样式 | | title | String? | - | 抽屉的标题 | | titleWidget | Widget? | - | 抽屉的标题组件 | +| visible | bool? | - | 组件是否可见 | | width | double? | 280 | 宽度 | ``` ``` -### TDrawerItem -#### 简介 -抽屉里的列表项 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| content | Widget? | - | 完全自定义 | -| icon | Widget? | - | 每列图标 | -| title | String? | - | 每列标题 | - -``` -``` - -### TDrawer +### TDrawerWidget #### 简介 -抽屉组件 +抽屉内容组件 + 可用于 Scaffold 中的 drawer 属性 #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | backgroundColor | Color? | - | 组件背景颜色 | | bordered | bool? | true | 是否显示边框 | -| closeOnOverlayClick | bool? | true | 点击蒙层时是否关闭抽屉 | | contentWidget | Widget? | - | 自定义内容,优先级高于[items]/[footer]/[title] | -| context | BuildContext | context | 上下文 | -| drawerTop | double? | - | 距离顶部的距离 | | footer | Widget? | - | 抽屉的底部 | | hover | bool? | true | 是否开启点击反馈 | | isShowLastBordered | bool? | true | 是否显示最后一行分割线 | | items | List? | - | 抽屉里的列表项 | -| onClose | VoidCallback? | - | 关闭时触发 | +| key | | - | | | onItemClick | TDrawerItemClickCallback? | - | 点击抽屉里的列表项触发 | -| placement | TDrawerPlacement? | TDrawerPlacement.right | 抽屉方向 | -| showOverlay | bool? | true | 是否显示遮罩层 | | style | TCellStyle? | - | 列表自定义样式 | | title | String? | - | 抽屉的标题 | | titleWidget | Widget? | - | 抽屉的标题组件 | -| visible | bool? | - | 组件是否可见 | | width | double? | 280 | 宽度 | +``` +``` + +### TDrawerItem +#### 简介 +抽屉里的列表项 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| content | Widget? | - | 完全自定义 | +| icon | Widget? | - | 每列图标 | +| title | String? | - | 每列标题 | + \ No newline at end of file diff --git a/tdesign-site/src/dropdown-menu/README.md b/tdesign-site/src/dropdown-menu/README.md index a8da01f44..ec977c8ed 100644 --- a/tdesign-site/src/dropdown-menu/README.md +++ b/tdesign-site/src/dropdown-menu/README.md @@ -266,9 +266,31 @@ TDropdownMenu _buildGroup(BuildContext context) { ## API -### TDropdownItemController +### TDropdownMenu #### 简介 -下拉菜单控制器 +下拉菜单 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| arrowColor | Color? | - | 自定义箭头颜色 | +| arrowIcon | IconData? | - | 自定义箭头图标 | +| builder | TDropdownItemBuilder? | - | 下拉菜单构建器,优先级高于[items] | +| closeOnClickOverlay | bool? | true | 是否在点击遮罩层后关闭菜单 | +| decoration | Decoration? | - | 下拉菜单的装饰器 | +| direction | TDropdownMenuDirection? | TDropdownMenuDirection.auto | 菜单展开方向(down、up、auto) | +| duration | double? | 200.0 | 动画时长,毫秒 | +| height | double? | 48 | menu的高度 | +| isScrollable | bool? | false | 是否开启滚动列表 | +| items | List? | - | 下拉菜单 | +| key | | - | | +| labelBuilder | LabelBuilder? | - | 自定义标签内容 | +| onMenuClosed | ValueChanged? | - | 关闭菜单事件 | +| onMenuOpened | ValueChanged? | - | 展开菜单事件 | +| showOverlay | bool? | true | 是否显示遮罩层 | +| tabBarAlign | MainAxisAlignment? | MainAxisAlignment.center | [TDropdownItem.label]和[arrowIcon]/[TDropdownItem.arrowIcon]的对齐方式 | +| width | double? | - | menu的宽度 | + ``` ``` @@ -319,30 +341,8 @@ TDropdownMenu _buildGroup(BuildContext context) { ``` ``` -### TDropdownMenu +### TDropdownItemController #### 简介 -下拉菜单 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| arrowColor | Color? | - | 自定义箭头颜色 | -| arrowIcon | IconData? | - | 自定义箭头图标 | -| builder | TDropdownItemBuilder? | - | 下拉菜单构建器,优先级高于[items] | -| closeOnClickOverlay | bool? | true | 是否在点击遮罩层后关闭菜单 | -| decoration | Decoration? | - | 下拉菜单的装饰器 | -| direction | TDropdownMenuDirection? | TDropdownMenuDirection.auto | 菜单展开方向(down、up、auto) | -| duration | double? | 200.0 | 动画时长,毫秒 | -| height | double? | 48 | menu的高度 | -| isScrollable | bool? | false | 是否开启滚动列表 | -| items | List? | - | 下拉菜单 | -| key | | - | | -| labelBuilder | LabelBuilder? | - | 自定义标签内容 | -| onMenuClosed | ValueChanged? | - | 关闭菜单事件 | -| onMenuOpened | ValueChanged? | - | 展开菜单事件 | -| showOverlay | bool? | true | 是否显示遮罩层 | -| tabBarAlign | MainAxisAlignment? | MainAxisAlignment.center | [TDropdownItem.label]和[arrowIcon]/[TDropdownItem.arrowIcon]的对齐方式 | -| width | double? | - | menu的宽度 | - +下拉菜单控制器 \ No newline at end of file diff --git a/tdesign-site/src/form/README.md b/tdesign-site/src/form/README.md index 4ec9a4528..25d02941b 100644 --- a/tdesign-site/src/form/README.md +++ b/tdesign-site/src/form/README.md @@ -709,6 +709,35 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ## API +### TForm +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| btnGroup | List? | - | 表单按钮组 | +| colon | bool? | false | 是否在表单标签字段右侧显示冒号 | +| data | Map | - | 表单数据 | +| disabled | bool | false | 是否禁用整个表单 | +| errorMessage | Object? | - | 表单信息错误信息配置 | +| formContentAlign | TextAlign | TextAlign.left | 表单内容对齐方式: 左对齐、右对齐、居中对齐 | +| formController | FormController? | - | 表单控制器 | +| formLabelAlign | TextAlign? | TextAlign.left | 表单字段标签的对齐方式: | +| formShowErrorMessage | bool? | true | 校验不通过时,是否显示错误提示信息,统一控制全部表单项 | +| isHorizontal | bool | true | 表单排列方式是否为 水平方向 | +| items | List | - | 表单内容 items | +| key | | - | | +| labelWidth | double? | 20.0 | 可以整体设置 label 标签宽度 | +| onReset | Function? | - | 表单重置时触发 | +| onSubmit | Function | - | 表单提交时触发 | +| preventSubmitDefault | bool? | true | 是否阻止表单提交默认事件(表单提交默认事件会刷新页面) | +| requiredMark | bool? | true | 是否显示必填符号(*),默认显示 | +| rules | Map | - | 整个表单字段校验规则 | +| scrollToFirstError | String? | - | 表单校验不通过时,是否自动滚动到第一个校验不通过的字段,平滑滚动或是瞬间直达。 | +| submitWithWarningMessage | bool? | false | 【讨论中】当校验结果只有告警信息时,是否触发 submit 提交事件 | + +``` +``` + ### TFormItem #### 默认构造方法 @@ -750,34 +779,5 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | type | TFormItemType | - | 校验对象的类型 | | validate | String? Function(dynamic) | - | 校验方法 | -``` -``` - -### TForm -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| btnGroup | List? | - | 表单按钮组 | -| colon | bool? | false | 是否在表单标签字段右侧显示冒号 | -| data | Map | - | 表单数据 | -| disabled | bool | false | 是否禁用整个表单 | -| errorMessage | Object? | - | 表单信息错误信息配置 | -| formContentAlign | TextAlign | TextAlign.left | 表单内容对齐方式: 左对齐、右对齐、居中对齐 | -| formController | FormController? | - | 表单控制器 | -| formLabelAlign | TextAlign? | TextAlign.left | 表单字段标签的对齐方式: | -| formShowErrorMessage | bool? | true | 校验不通过时,是否显示错误提示信息,统一控制全部表单项 | -| isHorizontal | bool | true | 表单排列方式是否为 水平方向 | -| items | List | - | 表单内容 items | -| key | | - | | -| labelWidth | double? | 20.0 | 可以整体设置 label 标签宽度 | -| onReset | Function? | - | 表单重置时触发 | -| onSubmit | Function | - | 表单提交时触发 | -| preventSubmitDefault | bool? | true | 是否阻止表单提交默认事件(表单提交默认事件会刷新页面) | -| requiredMark | bool? | true | 是否显示必填符号(*),默认显示 | -| rules | Map | - | 整个表单字段校验规则 | -| scrollToFirstError | String? | - | 表单校验不通过时,是否自动滚动到第一个校验不通过的字段,平滑滚动或是瞬间直达。 | -| submitWithWarningMessage | bool? | false | 【讨论中】当校验结果只有告警信息时,是否触发 submit 提交事件 | - \ No newline at end of file diff --git a/tdesign-site/src/image-viewer/README.md b/tdesign-site/src/image-viewer/README.md index fea213e97..4f159dc3e 100644 --- a/tdesign-site/src/image-viewer/README.md +++ b/tdesign-site/src/image-viewer/README.md @@ -69,6 +69,17 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ## API +### TImageViewer + +#### 静态方法 + +| 名称 | 返回类型 | 参数 | 说明 | +| --- | --- | --- | --- | +| showImageViewer | | required BuildContext context, required List images, List? labels, bool? closeBtn, bool? deleteBtn, bool? showIndex, bool? loop, bool? autoplay, int? duration, Color? bgColor, Color? navBarBgColor, Color? iconColor, TextStyle? labelStyle, TextStyle? indexStyle, Color? modalBarrierColor, bool? barrierDismissible, int? defaultIndex, double? width, double? height, OnIndexChange? onIndexChange, OnClose? onClose, OnDelete? onDelete, bool? ignoreDeleteError, OnImageTap? onTap, OnLongPress? onLongPress, LeftItemBuilder? leftItemBuilder, RightItemBuilder? rightItemBuilder, | 显示图片预览 | + +``` +``` + ### TImageViewerWidget #### 默认构造方法 @@ -100,16 +111,5 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | showIndex | bool? | - | 是否显示页码 | | width | double? | - | 图片宽度 | -``` -``` - -### TImageViewer - -#### 静态方法 - -| 名称 | 返回类型 | 参数 | 说明 | -| --- | --- | --- | --- | -| showImageViewer | | required BuildContext context, required List images, List? labels, bool? closeBtn, bool? deleteBtn, bool? showIndex, bool? loop, bool? autoplay, int? duration, Color? bgColor, Color? navBarBgColor, Color? iconColor, TextStyle? labelStyle, TextStyle? indexStyle, Color? modalBarrierColor, bool? barrierDismissible, int? defaultIndex, double? width, double? height, OnIndexChange? onIndexChange, OnClose? onClose, OnDelete? onDelete, bool? ignoreDeleteError, OnImageTap? onTap, OnLongPress? onLongPress, LeftItemBuilder? leftItemBuilder, RightItemBuilder? rightItemBuilder, | 显示图片预览 | - \ No newline at end of file diff --git a/tdesign-site/src/indexes/README.md b/tdesign-site/src/indexes/README.md index 1d3fb2fb0..991028b4b 100644 --- a/tdesign-site/src/indexes/README.md +++ b/tdesign-site/src/indexes/README.md @@ -205,6 +205,30 @@ Widget _buildOther(BuildContext context) { ## API +### TIndexes +#### 简介 +索引 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| builderAnchor | Widget? Function(BuildContext context, String index, bool isPinnedToTop)? | - | 锚点自定义构建 | +| builderContent | Widget? Function(BuildContext context, String index) | - | 内容自定义构建 | +| builderIndex | Widget Function(BuildContext context, String index, bool isActive)? | - | 索引文本自定义构建,包括索引激活左侧提示 | +| capsuleTheme | bool? | false | 锚点是否为胶囊式样式 | +| indexList | List? | - | 索引字符列表。不传默认 A-Z | +| indexListMaxHeight | double? | 0.8 | 索引列表最大高度(父容器高度的百分比,默认 0.8) | +| key | | - | | +| onChange | void Function(String index)? | - | 索引发生变更时触发事件 | +| onSelect | void Function(String index)? | - | 点击侧边栏时触发事件 | +| reverse | bool? | false | 反方向滚动置顶 | +| scrollController | ScrollController? | - | 滚动控制器 | +| sticky | bool? | true | 锚点是否吸顶 | +| stickyOffset | double? | 0 | 锚点吸顶时与顶部的距离 | + +``` +``` + ### TIndexesAnchor #### 简介 索引锚点 @@ -236,29 +260,5 @@ Widget _buildOther(BuildContext context) { | key | | - | | | onSelect | void Function(String newIndex, String oldIndex) | - | 点击侧边栏时触发事件 | -``` -``` - -### TIndexes -#### 简介 -索引 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| builderAnchor | Widget? Function(BuildContext context, String index, bool isPinnedToTop)? | - | 锚点自定义构建 | -| builderContent | Widget? Function(BuildContext context, String index) | - | 内容自定义构建 | -| builderIndex | Widget Function(BuildContext context, String index, bool isActive)? | - | 索引文本自定义构建,包括索引激活左侧提示 | -| capsuleTheme | bool? | false | 锚点是否为胶囊式样式 | -| indexList | List? | - | 索引字符列表。不传默认 A-Z | -| indexListMaxHeight | double? | 0.8 | 索引列表最大高度(父容器高度的百分比,默认 0.8) | -| key | | - | | -| onChange | void Function(String index)? | - | 索引发生变更时触发事件 | -| onSelect | void Function(String index)? | - | 点击侧边栏时触发事件 | -| reverse | bool? | false | 反方向滚动置顶 | -| scrollController | ScrollController? | - | 滚动控制器 | -| sticky | bool? | true | 锚点是否吸顶 | -| stickyOffset | double? | 0 | 锚点吸顶时与顶部的距离 | - \ No newline at end of file diff --git a/tdesign-site/src/message/README.md b/tdesign-site/src/message/README.md index da6676df5..176eafef1 100644 --- a/tdesign-site/src/message/README.md +++ b/tdesign-site/src/message/README.md @@ -286,30 +286,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ## API -### MessageLink -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| color | Color? | - | 颜色 | -| name | String | - | 名称 | -| uri | Uri? | - | 资源链接 | - -``` -``` - -### MessageMarquee -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| delay | int? | - | 延迟时间(毫秒) | -| loop | int? | - | 循环次数 | -| speed | int? | - | 速度 | - -``` -``` - ### TMessage #### 默认构造方法 @@ -336,5 +312,29 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | --- | --- | --- | --- | | showMessage | | required BuildContext context, String? content, bool? visible, int? duration, dynamic closeBtn, dynamic icon, dynamic link, MessageMarquee? marquee, List? offset, MessageTheme? theme, VoidCallback? onCloseBtnClick, VoidCallback? onDurationEnd, VoidCallback? onLinkClick, | | +``` +``` + +### MessageMarquee +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| delay | int? | - | 延迟时间(毫秒) | +| loop | int? | - | 循环次数 | +| speed | int? | - | 速度 | + +``` +``` + +### MessageLink +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| color | Color? | - | 颜色 | +| name | String | - | 名称 | +| uri | Uri? | - | 资源链接 | + \ No newline at end of file diff --git a/tdesign-site/src/notice-bar/README.md b/tdesign-site/src/notice-bar/README.md index 00740aeaa..f9b95f595 100644 --- a/tdesign-site/src/notice-bar/README.md +++ b/tdesign-site/src/notice-bar/README.md @@ -285,30 +285,6 @@ Widget _cardNoticeBar(BuildContext context) { ## API -### TNoticeBarStyle -#### 简介 -公告栏样式 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| backgroundColor | Color? | - | 公告栏背景色 | -| context | BuildContext? | - | 上下文 | -| leftIconColor | Color? | - | 公告栏左侧图标颜色 | -| padding | EdgeInsetsGeometry? | - | 公告栏内边距 | -| rightIconColor | Color? | - | 公告栏右侧图标颜色 | -| textStyle | TextStyle? | - | 公告栏内容样式 | - - -#### 工厂构造方法 - -| 名称 | 说明 | -| --- | --- | -| TNoticeBarStyle.generateTheme | 根据主题生成样式 | - -``` -``` - ### TNoticeBar #### 简介 @@ -333,5 +309,29 @@ Widget _cardNoticeBar(BuildContext context) { | suffixIcon | IconData? | - | 右侧图标 | | theme | TNoticeBarTheme? | TNoticeBarTheme.info | 主题 | +``` +``` + +### TNoticeBarStyle +#### 简介 +公告栏样式 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| backgroundColor | Color? | - | 公告栏背景色 | +| context | BuildContext? | - | 上下文 | +| leftIconColor | Color? | - | 公告栏左侧图标颜色 | +| padding | EdgeInsetsGeometry? | - | 公告栏内边距 | +| rightIconColor | Color? | - | 公告栏右侧图标颜色 | +| textStyle | TextStyle? | - | 公告栏内容样式 | + + +#### 工厂构造方法 + +| 名称 | 说明 | +| --- | --- | +| TNoticeBarStyle.generateTheme | 根据主题生成样式 | + \ No newline at end of file diff --git a/tdesign-site/src/picker/README.md b/tdesign-site/src/picker/README.md index 701dbc78e..2dfa16264 100644 --- a/tdesign-site/src/picker/README.md +++ b/tdesign-site/src/picker/README.md @@ -480,6 +480,31 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ## API +### TPicker +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| cancel | Widget? | - | 工具栏左侧自定义插槽,默认使用 [TResourceDelegate.cancel] | +| confirm | Widget? | - | 工具栏右侧自定义插槽,默认使用 [TResourceDelegate.confirm] | +| disabled | bool | false | 是否禁用整个选择器(禁止滚动和操作),默认 false | +| height | double | 200 | 视窗高度,默认 200 | +| initialValue | List? | - | 初始选中值列表(按 value 匹配) | +| itemBuilder | ItemBuilderType? | - | 自定义子项构建器(disabled 项仍由内部统一渲染,不会走此 builder) | +| itemCount | int | 5 | 每屏显示 item 数,默认 5 | +| itemDistanceCalculator | ItemDistanceCalculator? | - | 自定义距离计算器(控制颜色/字重/字号随"离中心距离"的变化) | +| items | TPickerItems | - | 数据源(必填) | +| key | | - | | +| onCancel | VoidCallback? | - | 点击「取消」按钮回调 | +| onChange | void Function(TPickerValue)? | - | 值改变回调(滚动时实时触发) | +| onConfirm | void Function(TPickerValue)? | - | 点击「确认」按钮回调 | +| onLoad | void Function(TPickerLoadEvent)? | - | 列选中项变化的事件回调 | +| title | String? | - | 工具栏中部标题(可选,不传时中部留白) | +| titleWidget | Widget? | - | 工具栏中部自定义标题插槽 | + +``` +``` + ### TPickerOption #### 默认构造方法 @@ -503,32 +528,16 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ``` ``` -### TPicker +### TPickerLoadEvent #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| cancel | Widget? | - | 工具栏左侧自定义插槽,默认使用 [TResourceDelegate.cancel] | -| confirm | Widget? | - | 工具栏右侧自定义插槽,默认使用 [TResourceDelegate.confirm] | -| disabled | bool | false | 是否禁用整个选择器(禁止滚动和操作),默认 false | -| height | double | 200 | 视窗高度,默认 200 | -| initialValue | List? | - | 初始选中值列表(按 value 匹配) | -| itemBuilder | ItemBuilderType? | - | 自定义子项构建器(disabled 项仍由内部统一渲染,不会走此 builder) | -| itemCount | int | 5 | 每屏显示 item 数,默认 5 | -| itemDistanceCalculator | ItemDistanceCalculator? | - | 自定义距离计算器(控制颜色/字重/字号随"离中心距离"的变化) | -| items | TPickerItems | - | 数据源(必填) | -| key | | - | | -| onCancel | VoidCallback? | - | 点击「取消」按钮回调 | -| onChange | void Function(TPickerValue)? | - | 值改变回调(滚动时实时触发) | -| onConfirm | void Function(TPickerValue)? | - | 点击「确认」按钮回调 | -| onLoad | void Function(TPickerLoadEvent)? | - | 列选中项变化的事件回调 | -| title | String? | - | 工具栏中部标题(可选,不传时中部留白) | -| titleWidget | Widget? | - | 工具栏中部自定义标题插槽 | - -``` -``` +| column | int | - | 触发事件的列索引(0 表示第一列) | +| displayedCount | int | - | 当前列已展示的选项总数 | +| parentValue | dynamic | - | 当前列的父级选中值(联动模式下使用) | +| remaining | int | - | 距底部剩余的选项数(业务可用此值做"接近底部时加载"判断) | -### TPickerItems ``` ``` @@ -580,6 +589,10 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ``` ``` +### TPickerItems +``` +``` + ### TPickerKeys #### 默认构造方法 @@ -590,18 +603,5 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | label | String | 'label' | 展示文案对应的字段名,默认 `label` | | value | String | 'value' | 业务值对应的字段名,默认 `value` | -``` -``` - -### TPickerLoadEvent -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| column | int | - | 触发事件的列索引(0 表示第一列) | -| displayedCount | int | - | 当前列已展示的选项总数 | -| parentValue | dynamic | - | 当前列的父级选中值(联动模式下使用) | -| remaining | int | - | 距底部剩余的选项数(业务可用此值做"接近底部时加载"判断) | - \ No newline at end of file diff --git a/tdesign-site/src/popover/README.md b/tdesign-site/src/popover/README.md index fe8cb93bb..60e8283db 100644 --- a/tdesign-site/src/popover/README.md +++ b/tdesign-site/src/popover/README.md @@ -1275,6 +1275,19 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ## API +### TPopover +#### 简介 + + +#### 静态方法 + +| 名称 | 返回类型 | 参数 | 说明 | +| --- | --- | --- | --- | +| showPopover | | required BuildContext context, String? content, Widget? contentWidget, double offset, TPopoverTheme? theme, bool closeOnClickOutside, TPopoverPlacement? placement, bool? showArrow, double arrowSize, EdgeInsetsGeometry? padding, double? width, double? height, Color? overlayColor, OnTap? onTap, OnLongTap? onLongTap, BorderRadius? radius, | | + +``` +``` + ### TPopoverWidget #### 简介 @@ -1298,18 +1311,5 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | theme | TPopoverTheme? | - | 弹出气泡主题 | | width | double? | - | 内容宽度(包含padding,实际高度:height - paddingLeft - paddingRight) | -``` -``` - -### TPopover -#### 简介 - - -#### 静态方法 - -| 名称 | 返回类型 | 参数 | 说明 | -| --- | --- | --- | --- | -| showPopover | | required BuildContext context, String? content, Widget? contentWidget, double offset, TPopoverTheme? theme, bool closeOnClickOutside, TPopoverPlacement? placement, bool? showArrow, double arrowSize, EdgeInsetsGeometry? padding, double? width, double? height, Color? overlayColor, OnTap? onTap, OnLongTap? onLongTap, BorderRadius? radius, | | - \ No newline at end of file diff --git a/tdesign-site/src/popup/README.md b/tdesign-site/src/popup/README.md index 9c3e12c11..ec55fb953 100644 --- a/tdesign-site/src/popup/README.md +++ b/tdesign-site/src/popup/README.md @@ -459,6 +459,32 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ## API +### TSlidePopupRoute +#### 简介 +从屏幕的某个方向滑动弹出的Dialog框的路由,比如从顶部、底部、左、右滑出页面 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| barrierClick | VoidCallback? | - | 蒙层点击事件,仅在[modalBarrierFull]为false时触发 | +| barrierLabel | | - | | +| builder | WidgetBuilder | - | 控件构建器 | +| close | VoidCallback? | - | 关闭前事件 | +| focusMove | bool | false | 是否有输入框获取焦点时整体平移避免输入框被遮挡 | +| isDismissible | bool | true | 点击蒙层能否关闭 | +| modalBarrierColor | Color? | Colors.black54 | 蒙层颜色 | +| modalBarrierFull | bool | false | 是否全屏显示蒙层 | +| modalHeight | double? | - | 弹出框高度 | +| modalLeft | double? | 0 | 弹出框左侧距离 | +| modalTop | double? | 0 | 弹出框顶部距离 | +| modalWidth | double? | - | 弹出框宽度 | +| open | VoidCallback? | - | 打开前事件 | +| opened | VoidCallback? | - | 打开后事件 | +| slideTransitionFrom | SlideTransitionFrom | SlideTransitionFrom.bottom | 设置从屏幕的哪个方向滑出 | + +``` +``` + ### TPopupBottomDisplayPanel #### 简介 右上角带关闭的底部浮层面板 @@ -530,31 +556,5 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | key | | - | | | radius | double? | - | 圆角 | -``` -``` - -### TSlidePopupRoute -#### 简介 -从屏幕的某个方向滑动弹出的Dialog框的路由,比如从顶部、底部、左、右滑出页面 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| barrierClick | VoidCallback? | - | 蒙层点击事件,仅在[modalBarrierFull]为false时触发 | -| barrierLabel | | - | | -| builder | WidgetBuilder | - | 控件构建器 | -| close | VoidCallback? | - | 关闭前事件 | -| focusMove | bool | false | 是否有输入框获取焦点时整体平移避免输入框被遮挡 | -| isDismissible | bool | true | 点击蒙层能否关闭 | -| modalBarrierColor | Color? | Colors.black54 | 蒙层颜色 | -| modalBarrierFull | bool | false | 是否全屏显示蒙层 | -| modalHeight | double? | - | 弹出框高度 | -| modalLeft | double? | 0 | 弹出框左侧距离 | -| modalTop | double? | 0 | 弹出框顶部距离 | -| modalWidth | double? | - | 弹出框宽度 | -| open | VoidCallback? | - | 打开前事件 | -| opened | VoidCallback? | - | 打开后事件 | -| slideTransitionFrom | SlideTransitionFrom | SlideTransitionFrom.bottom | 设置从屏幕的哪个方向滑出 | - \ No newline at end of file diff --git a/tdesign-site/src/side-bar/README.md b/tdesign-site/src/side-bar/README.md index b41fed384..9e5e9cc99 100644 --- a/tdesign-site/src/side-bar/README.md +++ b/tdesign-site/src/side-bar/README.md @@ -635,22 +635,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ## API -### TSideBarItem -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| badge | TBadge? | - | 徽标 | -| disabled | bool | false | 是否禁用 | -| icon | IconData? | - | 图标 | -| key | | - | | -| label | String | '' | 标签 | -| textStyle | TextStyle? | - | 标签样式 | -| value | int | -1 | 值 | - -``` -``` - ### TSideBar #### 默认构造方法 @@ -674,5 +658,21 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | unSelectedColor | Color? | - | 未选中颜色 | | value | int? | - | 选项值 | +``` +``` + +### TSideBarItem +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| badge | TBadge? | - | 徽标 | +| disabled | bool | false | 是否禁用 | +| icon | IconData? | - | 图标 | +| key | | - | | +| label | String | '' | 标签 | +| textStyle | TextStyle? | - | 标签样式 | +| value | int | -1 | 值 | + \ No newline at end of file diff --git a/tdesign-site/src/steps/README.md b/tdesign-site/src/steps/README.md index 5d49bdb41..2209c4d34 100644 --- a/tdesign-site/src/steps/README.md +++ b/tdesign-site/src/steps/README.md @@ -703,21 +703,6 @@ Vertical Customize Steps 垂直自定义步骤条 ## API -### TStepsItemData -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| content | String? | - | 内容 | -| customContent | Widget? | - | 自定义内容 | -| customTitle | Widget? | - | 自定义标题 | -| errorIcon | IconData? | - | 失败图标 | -| successIcon | IconData? | - | 成功图标 | -| title | String? | - | 标题 | - -``` -``` - ### TSteps #### 默认构造方法 @@ -732,5 +717,20 @@ Vertical Customize Steps 垂直自定义步骤条 | steps | List | - | 步骤条数据 | | verticalSelect | bool | false | 步骤条垂直自定义步骤条选择模式 | +``` +``` + +### TStepsItemData +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| content | String? | - | 内容 | +| customContent | Widget? | - | 自定义内容 | +| customTitle | Widget? | - | 自定义标题 | +| errorIcon | IconData? | - | 失败图标 | +| successIcon | IconData? | - | 成功图标 | +| title | String? | - | 标题 | + \ No newline at end of file diff --git a/tdesign-site/src/swiper/README.md b/tdesign-site/src/swiper/README.md index 5d07eb6db..87b5dda46 100644 --- a/tdesign-site/src/swiper/README.md +++ b/tdesign-site/src/swiper/README.md @@ -267,6 +267,21 @@ import 'package:flutter_swiper_null_safety/flutter_swiper_null_safety.dart'; ## API +### TSwiperPagination +#### 简介 +TDesign风格的Swiper指示器样式,与flutter_swiper的Swiper结合使用 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| alignment | Alignment? | - | 当 scrollDirection== Axis.horizontal 时,默认Alignment.bottomCenter | +| builder | SwiperPlugin | TSwiperPagination.dots | 具体样式 | +| key | Key? | - | | +| margin | EdgeInsetsGeometry | const EdgeInsets.all(10.0) | 指示器和container之间的距离 | + +``` +``` + ### TPageTransformer #### 简介 TD默认PageTransformer @@ -286,20 +301,5 @@ TD默认PageTransformer | TPageTransformer.margin | 普通margin的卡片式 | | TPageTransformer.scaleAndFade | 缩放或透明的卡片式 | -``` -``` - -### TSwiperPagination -#### 简介 -TDesign风格的Swiper指示器样式,与flutter_swiper的Swiper结合使用 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| alignment | Alignment? | - | 当 scrollDirection== Axis.horizontal 时,默认Alignment.bottomCenter | -| builder | SwiperPlugin | TSwiperPagination.dots | 具体样式 | -| key | Key? | - | | -| margin | EdgeInsetsGeometry | const EdgeInsets.all(10.0) | 指示器和container之间的距离 | - \ No newline at end of file diff --git a/tdesign-site/src/tab-bar/README.md b/tdesign-site/src/tab-bar/README.md index e85926081..f17afd5e9 100644 --- a/tdesign-site/src/tab-bar/README.md +++ b/tdesign-site/src/tab-bar/README.md @@ -571,38 +571,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ## API -### BadgeConfig -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| badgeRightOffset | double? | - | 消息右侧偏移量 | -| badgeTopOffset | double? | - | 消息顶部偏移量 | -| showBadge | bool | - | 是否展示消息 | -| tBadge | TBadge? | - | 消息样式(未设置但 showBadge 为 true,则默认使用红点) | - -``` -``` - -### TBottomTabBarTabConfig -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| allowMultipleTaps | bool | false | onTap 方法允许点击多次 | -| badgeConfig | BadgeConfig? | - | 消息配置 | -| onLongPress | GestureLongPressCallback? | - | 长按事件 | -| onTap | GestureTapCallback? | - | tab点击事件 | -| popUpButtonConfig | TBottomTabBarPopUpBtnConfig? | - | 弹窗配置 | -| selectedIcon | Widget? | - | 选中时图标 | -| selectTabTextStyle | TextStyle? | - | 文本已选择样式 basicType为text时必填 | -| tabText | String? | - | tab 文本 | -| unselectedIcon | Widget? | - | 未选中时图标 | -| unselectTabTextStyle | TextStyle? | - | 文本未选择样式 basicType为text时必填 | - -``` -``` - ### TBottomTabBar #### 默认构造方法 @@ -635,6 +603,38 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ``` ``` +### BadgeConfig +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| badgeRightOffset | double? | - | 消息右侧偏移量 | +| badgeTopOffset | double? | - | 消息顶部偏移量 | +| showBadge | bool | - | 是否展示消息 | +| tBadge | TBadge? | - | 消息样式(未设置但 showBadge 为 true,则默认使用红点) | + +``` +``` + +### TBottomTabBarTabConfig +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| allowMultipleTaps | bool | false | onTap 方法允许点击多次 | +| badgeConfig | BadgeConfig? | - | 消息配置 | +| onLongPress | GestureLongPressCallback? | - | 长按事件 | +| onTap | GestureTapCallback? | - | tab点击事件 | +| popUpButtonConfig | TBottomTabBarPopUpBtnConfig? | - | 弹窗配置 | +| selectedIcon | Widget? | - | 选中时图标 | +| selectTabTextStyle | TextStyle? | - | 文本已选择样式 basicType为text时必填 | +| tabText | String? | - | tab 文本 | +| unselectedIcon | Widget? | - | 未选中时图标 | +| unselectTabTextStyle | TextStyle? | - | 文本未选择样式 basicType为text时必填 | + +``` +``` + ### TBottomTabBarPopUpBtnConfig #### 默认构造方法 diff --git a/tdesign-site/src/tabs/README.md b/tdesign-site/src/tabs/README.md index aaeb9618b..a484905e1 100644 --- a/tdesign-site/src/tabs/README.md +++ b/tdesign-site/src/tabs/README.md @@ -308,19 +308,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ## API -### TTabBarView -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| children | List | - | 子widget列表 | -| controller | TabController? | - | 控制器 | -| isSlideSwitch | bool | false | 是否可以滑动切换 | -| key | | - | | - -``` -``` - ### TTabBar #### 默认构造方法 @@ -375,5 +362,18 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | text | String? | - | 文字内容 | | textMargin | EdgeInsetsGeometry? | - | 中间内容宽度 | +``` +``` + +### TTabBarView +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| children | List | - | 子widget列表 | +| controller | TabController? | - | 控制器 | +| isSlideSwitch | bool | false | 是否可以滑动切换 | +| key | | - | | + \ No newline at end of file diff --git a/tdesign-site/src/time-counter/README.md b/tdesign-site/src/time-counter/README.md index 790bed559..f7dec7c0a 100644 --- a/tdesign-site/src/time-counter/README.md +++ b/tdesign-site/src/time-counter/README.md @@ -608,6 +608,31 @@ TTimeCounter _buildCustomUnitLargeSize(BuildContext context) { ## API +### TTimeCounter +#### 简介 +计时组件 +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| autoStart | bool | true | 是否自动开始倒计时 | +| content | dynamic | 'default' | 'default' / Widget Function(int time) / Widget | +| controller | TTimeCounterController? | - | 控制器,可控制开始/暂停/继续/重置 | +| direction | TTimeCounterDirection | TTimeCounterDirection.down | 计时方向,默认倒计时 | +| format | String | 'HH:mm:ss' | 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒(分隔符必须为长度为1的非空格的字符) | +| key | | - | | +| millisecond | bool | false | 是否开启毫秒级渲染 | +| onChange | Function(int time)? | - | 时间变化时触发回调 | +| onFinish | VoidCallback? | - | 计时结束时触发回调 | +| size | TTimeCounterSize | TTimeCounterSize.medium | 尺寸 | +| splitWithUnit | bool | false | 使用时间单位分割 | +| style | TTimeCounterStyle? | - | 自定义样式,有则优先用它,没有则根据size和theme选取 | +| theme | TTimeCounterTheme | TTimeCounterTheme.defaultTheme | 风格 | +| time | int | - | 必需;计时时长,单位毫秒 | + +``` +``` + ### TTimeCounterController #### 简介 倒计时组件控制器,可控制开始(`start()`)/暂停(`pause()`)/继续(`resume()`)/重置(`reset([int? time])`) @@ -644,30 +669,5 @@ TTimeCounter _buildCustomUnitLargeSize(BuildContext context) { | --- | --- | | TTimeCounterStyle.generateStyle | 生成默认样式 | -``` -``` - -### TTimeCounter -#### 简介 -计时组件 -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| autoStart | bool | true | 是否自动开始倒计时 | -| content | dynamic | 'default' | 'default' / Widget Function(int time) / Widget | -| controller | TTimeCounterController? | - | 控制器,可控制开始/暂停/继续/重置 | -| direction | TTimeCounterDirection | TTimeCounterDirection.down | 计时方向,默认倒计时 | -| format | String | 'HH:mm:ss' | 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒(分隔符必须为长度为1的非空格的字符) | -| key | | - | | -| millisecond | bool | false | 是否开启毫秒级渲染 | -| onChange | Function(int time)? | - | 时间变化时触发回调 | -| onFinish | VoidCallback? | - | 计时结束时触发回调 | -| size | TTimeCounterSize | TTimeCounterSize.medium | 尺寸 | -| splitWithUnit | bool | false | 使用时间单位分割 | -| style | TTimeCounterStyle? | - | 自定义样式,有则优先用它,没有则根据size和theme选取 | -| theme | TTimeCounterTheme | TTimeCounterTheme.defaultTheme | 风格 | -| time | int | - | 必需;计时时长,单位毫秒 | - \ No newline at end of file diff --git a/tdesign-site/src/tree-select/README.md b/tdesign-site/src/tree-select/README.md index 5d824d1ea..dbb2ecb37 100644 --- a/tdesign-site/src/tree-select/README.md +++ b/tdesign-site/src/tree-select/README.md @@ -186,21 +186,6 @@ String类型ID(问题3) ## API -### TSelectOption -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| children | List | const [] | 子选项 | -| columnWidth | double? | - | 自定义宽度,允许用户指定每个选项的宽度 | -| label | String | - | 标签 | -| maxLines | int | 1 | 最大显示行数 | -| multiple | bool | false | 当前子项支持多选 | -| value | dynamic | - | 值 | - -``` -``` - ### TTreeSelect #### 默认构造方法 @@ -215,5 +200,20 @@ String类型ID(问题3) | outwardCornerRadius | double | 9 | 一级菜单选中项的外弯折圆角半径,默认为 9 | | style | TTreeSelectStyle | TTreeSelectStyle.normal | 一级菜单样式 | +``` +``` + +### TSelectOption +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| children | List | const [] | 子选项 | +| columnWidth | double? | - | 自定义宽度,允许用户指定每个选项的宽度 | +| label | String | - | 标签 | +| maxLines | int | 1 | 最大显示行数 | +| multiple | bool | false | 当前子项支持多选 | +| value | dynamic | - | 值 | + \ No newline at end of file From 33917d271b4e610239ee1858a9415b0747d5a9be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Wed, 13 May 2026 11:38:10 +0800 Subject: [PATCH 05/35] =?UTF-8?q?refactor(calendar):=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=8A=A8=E7=94=BB=E5=B8=B8=E9=87=8F=E5=B9=B6=E8=A1=A5=E5=85=85?= =?UTF-8?q?=20bottom=20=E6=96=87=E6=A1=A3=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/calendar/t_calendar.dart | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart index b12a30b79..cf6ceee2c 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart @@ -15,8 +15,12 @@ export 't_lunar_date.dart'; typedef CalendarFormat = TDate? Function(TDate? day); /// 底部自定义区域构建器 +/// /// [context] 当前上下文 /// [selectedDates] 当前选中的日期列表(毫秒时间戳) +/// +/// **仅在 popup 模式下** `selectedDates` 才会随用户点击实时更新; +/// 非 popup 模式下仅传入 [TCalendar.value] 的初始值。 typedef CalendarBottomBuilder = Widget Function( BuildContext context, List selectedDates, @@ -136,6 +140,10 @@ class TCalendar extends StatefulWidget { /// 底部自定义区域构建器,位于日历主体浮层上方。 /// + /// **注意:此属性仅在 popup 模式下生效**(即通过 [TCalendarPopup] 使用时)。 + /// 非 popup 模式下,由于缺少 [TCalendarInherited] 提供的响应式选中状态, + /// bottom 区域不会随用户点击自动更新 `selectedDates`。 + /// /// `selectedDates` 是当前选中日期的毫秒时间戳列表,会随用户点击单元格自动更新。 /// /// 典型用法:在 bottom 中渲染时间选择器、统计信息、操作按钮等。 @@ -205,6 +213,15 @@ class _TCalendarState extends State { /// (设计稿固定值,与 [_buildBottom] 的 bottom offset 配合) static const double _bottomPeekHeight = 30.0; + /// 确认按钮高度(TButton large 尺寸) + static const double _confirmBtnHeight = 48.0; + + /// bottom 展开/收起统一动画时长 + static const Duration _animDuration = Duration(milliseconds: 200); + + /// bottom 展开/收起统一动画曲线 + static const Curve _animCurve = Curves.easeInOut; + /// 标记是否已完成首次 selected 同步,避免 didChangeDependencies 重复触发 bool _initializedSelected = false; @@ -338,7 +355,8 @@ class _TCalendarState extends State { valueListenable: widget.bottomExpanded!, builder: (context, expanded, child) { return AnimatedPadding( - duration: const Duration(milliseconds: 200), + duration: _animDuration, + curve: _animCurve, padding: EdgeInsets.only( bottom: expanded ? _bottomPeekHeight : 0.0), child: child!, @@ -394,14 +412,19 @@ class _TCalendarState extends State { ); } - /// bottom 浮层:仅 AnimatedSlide 响应 expanded,其余内容通过 inherited.selected 驱动 + /// bottom 浮层:仅 AnimatedSlide 响应 expanded,其余内容通过 inherited.selected 驱动。 + /// + /// 注意:仅在 popup 模式(inherited != null)下 bottom 才能响应式更新选中日期; + /// 非 popup 模式下仅使用 widget.value 的初始快照,不会随用户点击刷新。 Widget _buildBottom() { + assert(widget.bottom != null); + final bottomOffset = (inherited?.usePopup == true) ? (widget.useSafeArea == true ? MediaQuery.of(context).padding.bottom + TTheme.of(context).spacer16 + - 48 - : TTheme.of(context).spacer16 * 2 + 48) + _confirmBtnHeight + : TTheme.of(context).spacer16 * 2 + _confirmBtnHeight) : (widget.useSafeArea == true ? MediaQuery.of(context).padding.bottom : 0.0); @@ -427,7 +450,8 @@ class _TCalendarState extends State { valueListenable: widget.bottomExpanded!, builder: (context, expanded, child) { return AnimatedSlide( - duration: const Duration(milliseconds: 200), + duration: _animDuration, + curve: _animCurve, offset: expanded ? Offset.zero : const Offset(0, 1), child: child!, ); From b824c1739595d881ef45cab0a03c3f4574830789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Wed, 13 May 2026 18:27:08 +0800 Subject: [PATCH 06/35] =?UTF-8?q?refactor(calendar):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E4=B8=8E?= =?UTF-8?q?=E5=BA=95=E9=83=A8=E5=8C=BA=E5=9F=9F=E5=93=8D=E5=BA=94=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/calendar/t_calendar.dart | 170 ++++++++++-------- .../components/calendar/t_calendar_popup.dart | 114 +++++++++--- 2 files changed, 191 insertions(+), 93 deletions(-) diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart index cf6ceee2c..89c66d322 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart @@ -14,13 +14,7 @@ export 't_lunar_date.dart'; typedef CalendarFormat = TDate? Function(TDate? day); -/// 底部自定义区域构建器 -/// -/// [context] 当前上下文 -/// [selectedDates] 当前选中的日期列表(毫秒时间戳) -/// -/// **仅在 popup 模式下** `selectedDates` 才会随用户点击实时更新; -/// 非 popup 模式下仅传入 [TCalendar.value] 的初始值。 +/// [TCalendar.bottom] 的构建器签名。`selectedDates` 为只读视图,详细行为见 [TCalendar.bottom]。 typedef CalendarBottomBuilder = Widget Function( BuildContext context, List selectedDates, @@ -28,8 +22,6 @@ typedef CalendarBottomBuilder = Widget Function( enum CalendarType { single, multiple, range } -enum CalendarTrigger { closeBtn, confirmBtn, overlay } - enum DateSelectType { selected, disabled, start, centre, end, empty } /// 日历组件 @@ -103,7 +95,7 @@ class TCalendar extends StatefulWidget { /// 宽度 final double? width; - ///锚点日期 + /// 锚点日期 final DateTime? anchorDate; /// 自定义样式 @@ -119,7 +111,7 @@ class TCalendar extends StatefulWidget { TDate tdate, )? onCellClick; - /// 长安日期时触发 + /// 长按日期时触发 final void Function( int value, DateSelectType type, @@ -135,27 +127,31 @@ class TCalendar extends StatefulWidget { /// 月份变化时触发 final ValueChanged? onMonthChange; - /// 是否使用安全区域,默认true + /// 是否使用安全区域(默认 true) final bool? useSafeArea; - /// 底部自定义区域构建器,位于日历主体浮层上方。 + /// 底部自定义区域构建器,以浮层方式叠加在日历主体之上。 /// - /// **注意:此属性仅在 popup 模式下生效**(即通过 [TCalendarPopup] 使用时)。 - /// 非 popup 模式下,由于缺少 [TCalendarInherited] 提供的响应式选中状态, - /// bottom 区域不会随用户点击自动更新 `selectedDates`。 + /// 适用于在日历下方渲染时间选择器、统计信息、操作按钮等。 /// - /// `selectedDates` 是当前选中日期的毫秒时间戳列表,会随用户点击单元格自动更新。 + /// - **不会撑高 [TCalendar]**,请在 [height] 中预留 bottom 自身的占用高度; + /// - 仅在 [TCalendarPopup] 模式下 `selectedDates` 会随点击实时更新, + /// 非 popup 模式下为 [value] 的初始快照; + /// - 传入的 `selectedDates` 是只读视图([List.unmodifiable]),如需变更请通过 [onChange]。 /// - /// 典型用法:在 bottom 中渲染时间选择器、统计信息、操作按钮等。 + /// ```dart + /// TCalendar( + /// height: 600, + /// bottom: (ctx, dates) => MyFooter(selectedDates: dates), + /// ) + /// ``` final CalendarBottomBuilder? bottom; /// bottom 区域是否展开(响应式)。 /// - /// - 传 `null`(默认):bottom 始终展开 - /// - 传 `ValueListenable`:bottom 展开/收起会跟随该 listenable 变化播放滑动动画, - /// 常配合 [ValueNotifier] 实现"按钮触发展开/收起"。 + /// 为 `null`(默认)时 bottom 始终展开;传入 [ValueListenable] 时, + /// bottom 展开/收起将跟随其值变化播放滑动动画,常配合 [ValueNotifier] 使用。 /// - /// 示例: /// ```dart /// final expanded = ValueNotifier(false); /// TCalendar( @@ -206,25 +202,26 @@ class TCalendar extends StatefulWidget { class _TCalendarState extends State { late List weekdayNames; late List monthNames; - late TCalendarInherited? inherited; + TCalendarInherited? inherited; late TCalendarStyle _style; - /// bottom 展开时日历主体上移高度,露出 bottom "把手"区域 - /// (设计稿固定值,与 [_buildBottom] 的 bottom offset 配合) + List? _cachedValueDates; + + // 时间戳列表,规范化到当日 0 点(去时分秒)。 + List _cachedNormalizedValue = const []; + + // bottom 展开时日历主体上移的距离,露出 bottom 顶部"把手"区域。 static const double _bottomPeekHeight = 30.0; - /// 确认按钮高度(TButton large 尺寸) static const double _confirmBtnHeight = 48.0; - - /// bottom 展开/收起统一动画时长 static const Duration _animDuration = Duration(milliseconds: 200); - - /// bottom 展开/收起统一动画曲线 static const Curve _animCurve = Curves.easeInOut; - /// 标记是否已完成首次 selected 同步,避免 didChangeDependencies 重复触发 bool _initializedSelected = false; + // 进程内仅打印一次的提示标志(static 跨实例共享)。 + static bool _warnedNoInheritedForBottom = false; + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -253,32 +250,45 @@ class _TCalendarState extends State { context.resource.december, ]; _style = widget.style ?? TCalendarStyle.generateStyle(context); - // 仅首次挂载时同步 widget.value → inherited.selected if (!_initializedSelected) { _initializedSelected = true; - _syncSelectedToInherited(); + _refreshValueCache(); + _syncSelectedToInheritedSync(); } } @override void didUpdateWidget(covariant TCalendar oldWidget) { super.didUpdateWidget(oldWidget); - // 仅当 widget.value 内容变化时,才重置 inherited.selected if (!listEquals(oldWidget.value, widget.value)) { - _syncSelectedToInherited(); + _refreshValueCache(); + _syncSelectedToInheritedDeferred(); } } - /// 把 widget.value 同步到 inherited.selected(仅在首次或 value 变化时调用) - void _syncSelectedToInherited() { + void _refreshValueCache() { + _cachedValueDates = widget._value; + _cachedNormalizedValue = _getValue(widget.value ?? const []); + } + + // 仅在非 build phase 调用。 + void _syncSelectedToInheritedSync() { + if (inherited == null) { + return; + } + inherited!.selected.value = _getValue(widget.value ?? const []); + } + + // 适用于 build phase 调用,写操作延迟到下一帧。 + void _syncSelectedToInheritedDeferred() { if (inherited == null) { return; } WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) { + if (!mounted || inherited == null) { return; } - inherited!.selected.value = _getValue(widget.value ?? []); + inherited!.selected.value = _getValue(widget.value ?? const []); }); } @@ -340,17 +350,14 @@ class _TCalendarState extends State { ); } - /// 仅在此处监听 bottomExpanded,局部化重建范围(只影响 padding 动画) Widget _buildAnimatedBody(double verticalGap, bool hasBottom) { if (!hasBottom || widget.bottomExpanded == null) { - // 无 bottom 或始终展开 → 静态 padding final padding = hasBottom ? _bottomPeekHeight : 0.0; return Padding( padding: EdgeInsets.only(bottom: padding), child: _buildCalendarBody(verticalGap), ); } - // 响应式 → 仅 AnimatedPadding 重建 return ValueListenableBuilder( valueListenable: widget.bottomExpanded!, builder: (context, expanded, child) { @@ -359,7 +366,7 @@ class _TCalendarState extends State { curve: _animCurve, padding: EdgeInsets.only( bottom: expanded ? _bottomPeekHeight : 0.0), - child: child!, + child: child, ); }, child: _buildCalendarBody(verticalGap), @@ -373,7 +380,7 @@ class _TCalendarState extends State { maxDate: widget.maxDate, anchorDate: widget.anchorDate, minDate: widget.minDate, - value: widget._value, + value: _cachedValueDates, bodyPadding: _style.bodyPadding ?? TTheme.of(context).spacer16, displayFormat: widget.displayFormat ?? 'year month', monthNames: monthNames, @@ -394,11 +401,7 @@ class _TCalendarState extends State { type: widget.type ?? CalendarType.single, data: data, padding: verticalGap / 2, - onChange: (value) { - final time = _getValue(value); - inherited?.selected.value = time; - widget.onChange?.call(time); - }, + onChange: _handleCellChange, onCellClick: widget.onCellClick, onCellLongPress: widget.onCellLongPress, dateList: dateList, @@ -412,34 +415,44 @@ class _TCalendarState extends State { ); } - /// bottom 浮层:仅 AnimatedSlide 响应 expanded,其余内容通过 inherited.selected 驱动。 - /// - /// 注意:仅在 popup 模式(inherited != null)下 bottom 才能响应式更新选中日期; - /// 非 popup 模式下仅使用 widget.value 的初始快照,不会随用户点击刷新。 + void _handleCellChange(List value) { + final time = _getValue(value); + inherited?.selected.value = time; + widget.onChange?.call(time); + } + + // 行为约定详见 [TCalendar.bottom]。 Widget _buildBottom() { assert(widget.bottom != null); + assert(() { + if (inherited == null && !_warnedNoInheritedForBottom) { + _warnedNoInheritedForBottom = true; + debugPrint( + '[TCalendar] bottom 在非 TCalendarPopup 模式下不会响应式更新 selectedDates,' + '仅渲染 widget.value 的初始快照。如需响应式,请通过 onChange 自行管理状态。', + ); + } + return true; + }()); - final bottomOffset = (inherited?.usePopup == true) - ? (widget.useSafeArea == true - ? MediaQuery.of(context).padding.bottom + - TTheme.of(context).spacer16 + - _confirmBtnHeight - : TTheme.of(context).spacer16 * 2 + _confirmBtnHeight) - : (widget.useSafeArea == true - ? MediaQuery.of(context).padding.bottom - : 0.0); - - // bottom 内容:跟随选中日期更新 + final bottomOffset = _calcBottomOffset(); + + // popup 模式由 inherited.selected 驱动;非 popup 模式使用规范化后的初始快照。 final content = inherited != null ? ValueListenableBuilder>( valueListenable: inherited!.selected, builder: (context, selectedDates, _) { - return widget.bottom!(context, selectedDates); + return widget.bottom!( + context, + List.unmodifiable(selectedDates), + ); }, ) - : widget.bottom!(context, widget.value ?? []); + : widget.bottom!( + context, + List.unmodifiable(_cachedNormalizedValue), + ); - // 如果有 bottomExpanded listenable → 用 ValueListenableBuilder 局部化 slide 动画 if (widget.bottomExpanded != null) { return Positioned( left: 0, @@ -453,7 +466,7 @@ class _TCalendarState extends State { duration: _animDuration, curve: _animCurve, offset: expanded ? Offset.zero : const Offset(0, 1), - child: child!, + child: child, ); }, child: content, @@ -462,7 +475,6 @@ class _TCalendarState extends State { ); } - // 无 listenable → 始终展开 return Positioned( left: 0, right: 0, @@ -471,6 +483,23 @@ class _TCalendarState extends State { ); } + // 该值需与 build() 中 Column 底部区域(confirmBtn padding + safeArea)保持一致; + // 修改底部布局时需同步更新本方法。 + double _calcBottomOffset() { + final safeBottom = widget.useSafeArea == true + ? MediaQuery.of(context).padding.bottom + : 0.0; + + if (inherited?.usePopup == true) { + final btnPadding = widget.useSafeArea == true + ? TTheme.of(context).spacer16 + : TTheme.of(context).spacer16 * 2; + return safeBottom + btnPadding + _confirmBtnHeight; + } + + return safeBottom; + } + List _getValue(List value) { return value.map((e) { final date = DateTime.fromMillisecondsSinceEpoch(e); @@ -478,13 +507,10 @@ class _TCalendarState extends State { }).toList(); } - /// 获取有效的单元格高度 - /// 当显示农历信息时,需要更大的高度以容纳额外的文本 double _getEffectiveCellHeight() { if (widget.cellHeight != null) { return widget.cellHeight!; } - // 显示农历信息时使用更大的默认高度(80px 完全避免溢出) return widget.showLunarInfo ? 80 : 60; } } diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_popup.dart b/tdesign-component/lib/src/components/calendar/t_calendar_popup.dart index b31fb034f..533d6e09b 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar_popup.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar_popup.dart @@ -1,11 +1,13 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../../tdesign_flutter.dart'; typedef CalendarBuilder = Widget Function(BuildContext context); -enum CalendarTrigger { closeBtn, confirmBtn, overlay } - -/// 单元格组件popup模式 +/// 日历的弹窗模式控制器。 +/// +/// 通过底部滑出弹窗承载 [TCalendar],提供选中态托管、确认/关闭回调与 +/// 全局单例约束。 class TCalendarPopup { TCalendarPopup( this.context, { @@ -26,48 +28,67 @@ class TCalendarPopup { } } - /// 上下文 + /// 触发 popup 时的根 context,用于 [Navigator.of] 查找并 push 弹窗路由 final BuildContext context; - /// 距离顶部的距离 + /// 弹窗顶部距离屏幕顶部的偏移量 final double? top; - /// 自动关闭;在点击关闭按钮、确认按钮、遮罩层时自动关闭 + /// 是否在点击关闭按钮、确认按钮或遮罩层时自动关闭弹窗(默认 true) final bool? autoClose; - /// 自定义确认按钮 + /// 自定义确认按钮;为 null 时使用默认主色 [TButton] final Widget? confirmBtn; - /// 默认是否显示日历 + /// 是否在构造时立即调用 [show] 打开弹窗(默认 false) final bool? visible; - /// 关闭时触发 + /// 弹窗关闭后回调 final VoidCallback? onClose; - /// 控件构建器,优先级高于[child] + /// 日历构建器,优先级高于 [child] final CalendarBuilder? builder; - /// 日历控件 + /// 日历控件,当 [builder] 为 null 时使用 final TCalendar? child; - /// 点击确认按钮时触发 + /// 点击确认按钮时回调,参数为当前选中的日期时间戳列表(毫秒) final void Function(List value)? onConfirm; static TSlidePopupRoute? _calendarPopup; - /// 当前选中值 + // 校验 close() 调用方与 show() 是同一实例。 + static TCalendarPopup? _owner; + + // 选中态存储,通过 [TCalendarInherited] 暴露给子树。 final ValueNotifier> _selected = ValueNotifier>([]); + bool _closing = false; + bool get _autoClose => autoClose ?? true; - /// 当前选中值 + /// 当前选中的日期时间戳列表(毫秒) List get selected => _selected.value; - /// 打开日历 + /// 打开日历弹窗。 + /// + /// 全局同时只允许一个 [TCalendarPopup] 处于显示状态,重复调用将被忽略 + /// (debug 触发 assert,release 通过 [FlutterError.reportError] 上报)。 void show() { + assert(_calendarPopup == null, + '[TCalendarPopup] 已有日历弹窗正在显示,请先调用 close() 关闭后再 show()。'); if (_calendarPopup != null) { + FlutterError.reportError(FlutterErrorDetails( + exception: StateError( + '[TCalendarPopup] show() 被忽略:已有日历弹窗正在显示,' + '请先调用 close() 关闭后再 show()。', + ), + library: 'tdesign_flutter', + context: ErrorDescription('TCalendarPopup.show'), + )); return; } + _owner = this; _calendarPopup = TSlidePopupRoute( isDismissible: false, slideTransitionFrom: SlideTransitionFrom.bottom, @@ -78,14 +99,21 @@ class TCalendarPopup { } }, builder: (context) { - final childWidget = builder?.call(context) ?? child; + final built = builder?.call(context); + final childWidget = built ?? child; + if (childWidget == null) { + throw FlutterError( + '[TCalendarPopup] builder 返回 null 且未提供 child,' + '请检查 builder 实现或传入非空 child。', + ); + } return TCalendarInherited( selected: _selected, usePopup: true, confirmBtn: confirmBtn, onClose: _onClose, onConfirm: _onConfirm, - child: childWidget!, + child: childWidget, ); }, ); @@ -95,28 +123,54 @@ class TCalendarPopup { } void _onClose() { + if (_closing) { + return; + } if (_autoClose) { close(); } } void _onConfirm() { - onConfirm?.call(_selected.value); + if (_closing) { + return; + } + onConfirm?.call(List.from(_selected.value)); if (_autoClose) { close(); } } - /// 关闭日历 + /// 关闭日历弹窗。 + /// + /// 仅当本实例为当前 popup 的 owner 时才会真正 pop;重复调用会被忽略。 void close() { - if (_calendarPopup != null) { + final route = _calendarPopup; + if (route == null || _closing) { + return; + } + assert( + _owner == this, + '[TCalendarPopup] close() 被非 owner 实例调用,已忽略。' + '请确保 show() 与 close() 在同一实例上成对调用。', + ); + if (_owner != this) { + return; + } + + _closing = true; + final navigator = route.navigator; + if (navigator != null) { + navigator.pop(); + } else { Navigator.of(context).pop(); - // _deleteRouter(); } } void _deleteRouter() { _calendarPopup = null; + _owner = null; + _closing = false; onClose?.call(); } } @@ -133,13 +187,31 @@ class TCalendarInherited extends InheritedWidget { }) : super(child: child, key: key); final VoidCallback? onClose; + + /// 选中态的可写引用(仅供 [TCalendar] 内部更新使用)。 + /// + /// 对外消费方(如自定义 [confirmBtn] 或 [TCalendar.bottom])请使用 + /// [selectedListenable] 这一只读视图。 final ValueNotifier> selected; + + /// 选中态的只读视图,供下游 widget 监听变化。 + /// + /// ```dart + /// final inherited = TCalendarInherited.of(context); + /// return ValueListenableBuilder>( + /// valueListenable: inherited!.selectedListenable, + /// builder: (ctx, dates, _) => Text('已选 ${dates.length} 天'), + /// ); + /// ``` + ValueListenable> get selectedListenable => selected; + final bool? usePopup; final VoidCallback? onConfirm; final Widget? confirmBtn; @override bool updateShouldNotify(covariant TCalendarInherited oldWidget) { + // 选中态变化由 [selectedListenable] 通知,本 InheritedWidget 自身无需触发重建。 return false; } From c251b98be6785fe5475fc5bf7ed0723d5504a081 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 10:45:15 +0000 Subject: [PATCH 07/35] [autofix.ci] apply automated fixes --- .../example/assets/api/calendar_api.md | 24 +++++++++---------- tdesign-site/src/calendar/README.md | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md index fda9741d4..4a1211271 100644 --- a/tdesign-component/example/assets/api/calendar_api.md +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -6,7 +6,7 @@ | --- | --- | --- | --- | | anchorDate | DateTime? | - | 锚点日期 | | animateTo | bool? | false | 动画滚动到指定位置 | -| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,位于日历主体浮层上方。 | +| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,以浮层方式叠加在日历主体之上。 | | bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。 | | cellHeight | double? | 60 | 日期高度 | | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | @@ -22,7 +22,7 @@ | monthTitleBuilder | Widget Function(BuildContext context, DateTime monthDate)? | - | 月标题构建器 | | monthTitleHeight | double? | 22 | 月标题高度 | | onCellClick | void Function(int value, DateSelectType type, TDate tdate)? | - | 点击日期时触发 | -| onCellLongPress | void Function(int value, DateSelectType type, TDate tdate)? | - | 长安日期时触发 | +| onCellLongPress | void Function(int value, DateSelectType type, TDate tdate)? | - | 长按日期时触发 | | onChange | void Function(List value)? | - | 选中值变化时触发 | | onHeaderClick | void Function(int index, String week)? | - | 点击周时触发 | | onMonthChange | ValueChanged? | - | 月份变化时触发 | @@ -31,7 +31,7 @@ | title | String? | - | 标题 | | titleWidget | Widget? | - | 标题组件 | | type | CalendarType? | CalendarType.single | 日历的选择类型,single = 单选;multiple = 多选;range = 区间选择 | -| useSafeArea | bool? | true | 是否使用安全区域,默认true | +| useSafeArea | bool? | true | 是否使用安全区域(默认 true) | | value | List? | - | 当前选择的日期(fromMillisecondsSinceEpoch),不传则默认今天,当 type = single 时数组长度为1 | | width | double? | - | 宽度 | @@ -43,15 +43,15 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| autoClose | bool? | true | 自动关闭;在点击关闭按钮、确认按钮、遮罩层时自动关闭 | -| builder | CalendarBuilder? | - | 控件构建器,优先级高于[child] | -| child | TCalendar? | - | 日历控件 | -| confirmBtn | Widget? | - | 自定义确认按钮 | -| context | BuildContext | context | 上下文 | -| onClose | VoidCallback? | - | 关闭时触发 | -| onConfirm | void Function(List value)? | - | 点击确认按钮时触发 | -| top | double? | - | 距离顶部的距离 | -| visible | bool? | - | 默认是否显示日历 | +| autoClose | bool? | true | 是否在点击关闭按钮、确认按钮或遮罩层时自动关闭弹窗(默认 true) | +| builder | CalendarBuilder? | - | 日历构建器,优先级高于 [child] | +| child | TCalendar? | - | 日历控件,当 [builder] 为 null 时使用 | +| confirmBtn | Widget? | - | 自定义确认按钮;为 null 时使用默认主色 [TButton] | +| context | BuildContext | context | 触发 popup 时的根 context,用于 [Navigator.of] 查找并 push 弹窗路由 | +| onClose | VoidCallback? | - | 弹窗关闭后回调 | +| onConfirm | void Function(List value)? | - | 点击确认按钮时回调,参数为当前选中的日期时间戳列表(毫秒) | +| top | double? | - | 弹窗顶部距离屏幕顶部的偏移量 | +| visible | bool? | - | 是否在构造时立即调用 [show] 打开弹窗(默认 false) | ``` ``` diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md index 43875fc92..51cce744e 100644 --- a/tdesign-site/src/calendar/README.md +++ b/tdesign-site/src/calendar/README.md @@ -1251,7 +1251,7 @@ Widget _buildLunar(BuildContext context) { | --- | --- | --- | --- | | anchorDate | DateTime? | - | 锚点日期 | | animateTo | bool? | false | 动画滚动到指定位置 | -| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,位于日历主体浮层上方。 | +| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,以浮层方式叠加在日历主体之上。 | | bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。 | | cellHeight | double? | 60 | 日期高度 | | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | @@ -1267,7 +1267,7 @@ Widget _buildLunar(BuildContext context) { | monthTitleBuilder | Widget Function(BuildContext context, DateTime monthDate)? | - | 月标题构建器 | | monthTitleHeight | double? | 22 | 月标题高度 | | onCellClick | void Function(int value, DateSelectType type, TDate tdate)? | - | 点击日期时触发 | -| onCellLongPress | void Function(int value, DateSelectType type, TDate tdate)? | - | 长安日期时触发 | +| onCellLongPress | void Function(int value, DateSelectType type, TDate tdate)? | - | 长按日期时触发 | | onChange | void Function(List value)? | - | 选中值变化时触发 | | onHeaderClick | void Function(int index, String week)? | - | 点击周时触发 | | onMonthChange | ValueChanged? | - | 月份变化时触发 | @@ -1276,7 +1276,7 @@ Widget _buildLunar(BuildContext context) { | title | String? | - | 标题 | | titleWidget | Widget? | - | 标题组件 | | type | CalendarType? | CalendarType.single | 日历的选择类型,single = 单选;multiple = 多选;range = 区间选择 | -| useSafeArea | bool? | true | 是否使用安全区域,默认true | +| useSafeArea | bool? | true | 是否使用安全区域(默认 true) | | value | List? | - | 当前选择的日期(fromMillisecondsSinceEpoch),不传则默认今天,当 type = single 时数组长度为1 | | width | double? | - | 宽度 | @@ -1288,15 +1288,15 @@ Widget _buildLunar(BuildContext context) { | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| autoClose | bool? | true | 自动关闭;在点击关闭按钮、确认按钮、遮罩层时自动关闭 | -| builder | CalendarBuilder? | - | 控件构建器,优先级高于[child] | -| child | TCalendar? | - | 日历控件 | -| confirmBtn | Widget? | - | 自定义确认按钮 | -| context | BuildContext | context | 上下文 | -| onClose | VoidCallback? | - | 关闭时触发 | -| onConfirm | void Function(List value)? | - | 点击确认按钮时触发 | -| top | double? | - | 距离顶部的距离 | -| visible | bool? | - | 默认是否显示日历 | +| autoClose | bool? | true | 是否在点击关闭按钮、确认按钮或遮罩层时自动关闭弹窗(默认 true) | +| builder | CalendarBuilder? | - | 日历构建器,优先级高于 [child] | +| child | TCalendar? | - | 日历控件,当 [builder] 为 null 时使用 | +| confirmBtn | Widget? | - | 自定义确认按钮;为 null 时使用默认主色 [TButton] | +| context | BuildContext | context | 触发 popup 时的根 context,用于 [Navigator.of] 查找并 push 弹窗路由 | +| onClose | VoidCallback? | - | 弹窗关闭后回调 | +| onConfirm | void Function(List value)? | - | 点击确认按钮时回调,参数为当前选中的日期时间戳列表(毫秒) | +| top | double? | - | 弹窗顶部距离屏幕顶部的偏移量 | +| visible | bool? | - | 是否在构造时立即调用 [show] 打开弹窗(默认 false) | ``` ``` From d6c065db6e8632d3e62d846b7a5ebe51875c63ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Sun, 17 May 2026 20:18:26 +0800 Subject: [PATCH 08/35] =?UTF-8?q?chore(coverage):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=B7=B2=E5=88=A0=E9=99=A4=20t=5Fdate=5Fpicker=20=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=20lcov=20=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 日历重构后 date_picker_model 与 t_date_picker 已不存在,清理残留覆盖率条目。 Co-authored-by: Cursor --- tdesign-component/coverage/lcov.info | 160 --------------------------- 1 file changed, 160 deletions(-) diff --git a/tdesign-component/coverage/lcov.info b/tdesign-component/coverage/lcov.info index e24c20947..687912903 100644 --- a/tdesign-component/coverage/lcov.info +++ b/tdesign-component/coverage/lcov.info @@ -1849,100 +1849,6 @@ DA:204,0 LF:104 LH:0 end_of_record -SF:lib/src/components/calendar/date_picker_model.dart -DA:33,0 -DA:48,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:55,0 -DA:58,0 -DA:59,0 -DA:60,0 -DA:64,0 -DA:67,0 -DA:70,0 -DA:73,0 -DA:80,0 -DA:82,0 -DA:83,0 -DA:84,0 -DA:85,0 -DA:88,0 -DA:90,0 -DA:91,0 -DA:92,0 -DA:93,0 -DA:94,0 -DA:97,0 -DA:99,0 -DA:100,0 -DA:101,0 -DA:102,0 -DA:103,0 -DA:104,0 -DA:108,0 -DA:109,0 -DA:110,0 -DA:112,0 -DA:113,0 -DA:114,0 -DA:115,0 -DA:116,0 -DA:117,0 -DA:118,0 -DA:120,0 -DA:121,0 -DA:122,0 -DA:126,0 -DA:127,0 -DA:128,0 -DA:129,0 -DA:130,0 -DA:131,0 -DA:137,0 -DA:141,0 -DA:144,0 -DA:145,0 -DA:146,0 -DA:148,0 -DA:149,0 -DA:151,0 -DA:152,0 -DA:153,0 -DA:155,0 -DA:156,0 -DA:157,0 -DA:161,0 -DA:162,0 -DA:164,0 -DA:165,0 -DA:167,0 -DA:169,0 -DA:170,0 -DA:175,0 -DA:176,0 -DA:178,0 -DA:179,0 -DA:180,0 -DA:182,0 -DA:183,0 -DA:184,0 -DA:186,0 -DA:187,0 -DA:188,0 -DA:190,0 -DA:191,0 -DA:192,0 -DA:194,0 -DA:195,0 -DA:196,0 -DA:198,0 -DA:199,0 -DA:200,0 -LF:90 -LH:0 -end_of_record SF:lib/src/components/picker/no_wave_behavior.dart DA:11,1 DA:14,2 @@ -2210,72 +2116,6 @@ DA:432,0 LF:171 LH:0 end_of_record -SF:lib/src/components/calendar/t_date_picker.dart -DA:21,0 -DA:34,0 -DA:35,0 -DA:41,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:48,0 -DA:50,0 -DA:52,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:58,0 -DA:59,0 -DA:60,0 -DA:61,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:69,0 -DA:74,0 -DA:75,0 -DA:77,0 -DA:79,0 -DA:80,0 -DA:81,0 -DA:84,0 -DA:86,0 -DA:87,0 -DA:88,0 -DA:92,0 -DA:94,0 -DA:95,0 -DA:96,0 -DA:97,0 -DA:108,0 -DA:109,0 -DA:110,0 -DA:112,0 -DA:113,0 -DA:115,0 -DA:116,0 -DA:117,0 -DA:118,0 -DA:120,0 -DA:122,0 -DA:124,0 -DA:125,0 -DA:127,0 -DA:129,0 -DA:130,0 -DA:131,0 -DA:132,0 -DA:133,0 -DA:135,0 -DA:137,0 -DA:139,0 -DA:142,0 -LF:62 -LH:0 -end_of_record SF:lib/src/components/calendar/t_calendar_body.dart DA:10,0 DA:31,0 From 77c57e229a52242983f444d9c6cad0b7bbec2522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Sun, 17 May 2026 23:38:19 +0800 Subject: [PATCH 09/35] refactor(calendar): enhance bottom area handling and assert conditions --- .../example/lib/page/t_calendar_page.dart | 73 ++--- .../src/components/calendar/t_calendar.dart | 259 ++++++++++-------- tdesign-component/test/t_calendar_test.dart | 145 ++++++++++ 3 files changed, 326 insertions(+), 151 deletions(-) create mode 100644 tdesign-component/test/t_calendar_test.dart diff --git a/tdesign-component/example/lib/page/t_calendar_page.dart b/tdesign-component/example/lib/page/t_calendar_page.dart index fde0cd058..6b22a34f4 100644 --- a/tdesign-component/example/lib/page/t_calendar_page.dart +++ b/tdesign-component/example/lib/page/t_calendar_page.dart @@ -727,40 +727,51 @@ class _RangeTimePickerPanel extends StatefulWidget { } class _RangeTimePickerPanelState extends State<_RangeTimePickerPanel> { - late int _tab = widget.currentTab; + late int _tab; + + @override + void initState() { + super.initState(); + _tab = widget.currentTab; + } + + @override + void didUpdateWidget(covariant _RangeTimePickerPanel oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.currentTab != widget.currentTab) { + _tab = widget.currentTab; + } + } @override Widget build(BuildContext context) { - return DefaultTabController( - length: 2, - child: Container( - decoration: _bottomCardDecoration(context), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TTabBar( - height: 40, - showIndicator: true, - tabs: const [ - TTab(text: '开始时间'), - TTab(text: '结束时间'), - ], - onTap: (i) { - setState(() => _tab = i); - widget.onTabChanged(i); - }, - ), - TPicker( - key: ValueKey(_tab), - items: widget.items, - initialValue: widget.initialValues[_tab], - height: 180, - itemCount: 5, - onChange: (v) => - widget.onPickerChanged(_tab, List.from(v.values)), - ), - ], - ), + return Container( + decoration: _bottomCardDecoration(context), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TTabBar( + height: 40, + showIndicator: true, + tabs: const [ + TTab(text: '开始时间'), + TTab(text: '结束时间'), + ], + onTap: (i) { + setState(() => _tab = i); + widget.onTabChanged(i); + }, + ), + TPicker( + key: ValueKey(_tab), + items: widget.items, + initialValue: widget.initialValues[_tab], + height: 180, + itemCount: 5, + onChange: (v) => + widget.onPickerChanged(_tab, List.from(v.values)), + ), + ], ), ); } diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart index 89c66d322..586fbf024 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart @@ -57,7 +57,11 @@ class TCalendar extends StatefulWidget { this.dateType = TCalendarDateType.solar, this.dataSource, this.showLunarInfo = false, - }) : super(key: key); + }) : assert( + bottomExpanded == null || bottom != null, + 'bottomExpanded 需配合 bottom 使用', + ), + super(key: key); /// 第一天从星期几开始,默认 0 = 周日 final int? firstDayOfWeek; @@ -132,32 +136,37 @@ class TCalendar extends StatefulWidget { /// 底部自定义区域构建器,以浮层方式叠加在日历主体之上。 /// - /// 适用于在日历下方渲染时间选择器、统计信息、操作按钮等。 + /// **仅能在 [TCalendarPopup] 内使用**(作为其 [child] / [builder] 的子树)。 /// /// - **不会撑高 [TCalendar]**,请在 [height] 中预留 bottom 自身的占用高度; - /// - 仅在 [TCalendarPopup] 模式下 `selectedDates` 会随点击实时更新, - /// 非 popup 模式下为 [value] 的初始快照; - /// - 传入的 `selectedDates` 是只读视图([List.unmodifiable]),如需变更请通过 [onChange]。 + /// - `selectedDates` 随弹窗内日期点击实时更新; + /// - 传入的 `selectedDates` 是只读视图([List.unmodifiable]),请勿原地修改。 /// /// ```dart - /// TCalendar( - /// height: 600, - /// bottom: (ctx, dates) => MyFooter(selectedDates: dates), - /// ) + /// TCalendarPopup( + /// context, + /// child: TCalendar( + /// height: 600, + /// bottom: (ctx, dates) => MyFooter(selectedDates: dates), + /// ), + /// ); /// ``` final CalendarBottomBuilder? bottom; - /// bottom 区域是否展开(响应式)。 + /// bottom 区域是否展开(响应式)。**仅能在 [TCalendarPopup] 内使用。** /// /// 为 `null`(默认)时 bottom 始终展开;传入 [ValueListenable] 时, /// bottom 展开/收起将跟随其值变化播放滑动动画,常配合 [ValueNotifier] 使用。 /// /// ```dart /// final expanded = ValueNotifier(false); - /// TCalendar( - /// bottomExpanded: expanded, - /// onCellClick: (v, t, d) => expanded.value = true, - /// bottom: (ctx, dates) => MyFooter(), + /// TCalendarPopup( + /// context, + /// child: TCalendar( + /// bottomExpanded: expanded, + /// onCellClick: (v, t, d) => expanded.value = true, + /// bottom: (ctx, dates) => MyFooter(), + /// ), /// ); /// ``` final ValueListenable? bottomExpanded; @@ -207,9 +216,6 @@ class _TCalendarState extends State { List? _cachedValueDates; - // 时间戳列表,规范化到当日 0 点(去时分秒)。 - List _cachedNormalizedValue = const []; - // bottom 展开时日历主体上移的距离,露出 bottom 顶部"把手"区域。 static const double _bottomPeekHeight = 30.0; @@ -219,8 +225,7 @@ class _TCalendarState extends State { bool _initializedSelected = false; - // 进程内仅打印一次的提示标志(static 跨实例共享)。 - static bool _warnedNoInheritedForBottom = false; + bool get _usePopupBottom => inherited?.usePopup == true; @override void didChangeDependencies() { @@ -268,7 +273,17 @@ class _TCalendarState extends State { void _refreshValueCache() { _cachedValueDates = widget._value; - _cachedNormalizedValue = _getValue(widget.value ?? const []); + } + + void _assertPopupOnlyBottom() { + assert( + widget.bottom == null || _usePopupBottom, + '[TCalendar] bottom 仅能在 TCalendarPopup 内使用', + ); + assert( + widget.bottomExpanded == null || _usePopupBottom, + '[TCalendar] bottomExpanded 仅能在 TCalendarPopup 内使用', + ); } // 仅在非 build phase 调用。 @@ -294,82 +309,104 @@ class _TCalendarState extends State { @override Widget build(BuildContext context) { + _assertPopupOnlyBottom(); final verticalGap = _style.verticalGap ?? TTheme.of(context).spacer8; - final hasBottom = widget.bottom != null; + final hasBottom = widget.bottom != null && _usePopupBottom; + + Widget stackContent(bool bottomExpanded) { + return Stack( + children: [ + _buildMainColumn(verticalGap, hasBottom, bottomExpanded), + if (hasBottom) _buildBottom(bottomExpanded), + ], + ); + } + + final child = hasBottom && widget.bottomExpanded != null + ? ValueListenableBuilder( + valueListenable: widget.bottomExpanded!, + builder: (context, expanded, _) => stackContent(expanded), + ) + : stackContent(hasBottom); return Container( height: widget.height, width: widget.width ?? double.infinity, decoration: _style.decoration, - child: Stack( - children: [ - Column( - children: [ - TCalendarHeader( - firstDayOfWeek: widget.firstDayOfWeek ?? 0, - weekdayGap: TTheme.of(context).spacer4, - padding: TTheme.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.onHeaderClick, - ), - Expanded( - child: _buildAnimatedBody(verticalGap, hasBottom), + child: child, + ); + } + + Widget _buildMainColumn( + double verticalGap, + bool hasBottom, + bool bottomExpanded, + ) { + return Column( + children: [ + TCalendarHeader( + firstDayOfWeek: widget.firstDayOfWeek ?? 0, + weekdayGap: TTheme.of(context).spacer4, + padding: TTheme.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.onHeaderClick, + ), + Expanded( + child: _buildBodyArea(verticalGap, hasBottom, bottomExpanded), + ), + if (inherited?.usePopup == true) + inherited?.confirmBtn ?? + Padding( + padding: widget.useSafeArea == true + ? EdgeInsets.only(top: TTheme.of(context).spacer16) + : EdgeInsets.symmetric( + vertical: TTheme.of(context).spacer16), + child: TButton( + theme: TButtonTheme.primary, + text: context.resource.confirm, + isBlock: true, + size: TButtonSize.large, + onTap: inherited?.onConfirm, + ), ), - if (inherited?.usePopup == true) - inherited?.confirmBtn ?? - Padding( - padding: widget.useSafeArea == true - ? EdgeInsets.only(top: TTheme.of(context).spacer16) - : EdgeInsets.symmetric( - vertical: TTheme.of(context).spacer16), - child: TButton( - theme: TButtonTheme.primary, - text: context.resource.confirm, - isBlock: true, - size: TButtonSize.large, - onTap: inherited?.onConfirm, - ), - ), - if (widget.useSafeArea == true) - SizedBox(height: MediaQuery.of(context).padding.bottom) - ], - ), - if (hasBottom) _buildBottom(), - ], - ), + if (widget.useSafeArea == true) + SizedBox(height: MediaQuery.of(context).padding.bottom) + ], ); } - Widget _buildAnimatedBody(double verticalGap, bool hasBottom) { - if (!hasBottom || widget.bottomExpanded == null) { - final padding = hasBottom ? _bottomPeekHeight : 0.0; + Widget _buildBodyArea( + double verticalGap, + bool hasBottom, + bool bottomExpanded, + ) { + final body = _buildCalendarBody(verticalGap); + if (!hasBottom) { + return body; + } + if (widget.bottomExpanded == null) { return Padding( - padding: EdgeInsets.only(bottom: padding), - child: _buildCalendarBody(verticalGap), + padding: const EdgeInsets.only(bottom: _bottomPeekHeight), + child: body, ); } - return ValueListenableBuilder( - valueListenable: widget.bottomExpanded!, - builder: (context, expanded, child) { - return AnimatedPadding( - duration: _animDuration, - curve: _animCurve, - padding: EdgeInsets.only( - bottom: expanded ? _bottomPeekHeight : 0.0), - child: child, - ); - }, - child: _buildCalendarBody(verticalGap), + return AnimatedPadding( + duration: _animDuration, + curve: _animCurve, + padding: EdgeInsets.only( + bottom: bottomExpanded ? _bottomPeekHeight : 0.0, + ), + child: body, ); } @@ -422,36 +459,21 @@ class _TCalendarState extends State { } // 行为约定详见 [TCalendar.bottom]。 - Widget _buildBottom() { + Widget _buildBottom(bool bottomExpanded) { assert(widget.bottom != null); - assert(() { - if (inherited == null && !_warnedNoInheritedForBottom) { - _warnedNoInheritedForBottom = true; - debugPrint( - '[TCalendar] bottom 在非 TCalendarPopup 模式下不会响应式更新 selectedDates,' - '仅渲染 widget.value 的初始快照。如需响应式,请通过 onChange 自行管理状态。', - ); - } - return true; - }()); + assert(inherited != null); final bottomOffset = _calcBottomOffset(); - // popup 模式由 inherited.selected 驱动;非 popup 模式使用规范化后的初始快照。 - final content = inherited != null - ? ValueListenableBuilder>( - valueListenable: inherited!.selected, - builder: (context, selectedDates, _) { - return widget.bottom!( - context, - List.unmodifiable(selectedDates), - ); - }, - ) - : widget.bottom!( - context, - List.unmodifiable(_cachedNormalizedValue), - ); + final content = ValueListenableBuilder>( + valueListenable: inherited!.selected, + builder: (context, selectedDates, _) { + return widget.bottom!( + context, + List.unmodifiable(selectedDates), + ); + }, + ); if (widget.bottomExpanded != null) { return Positioned( @@ -459,16 +481,10 @@ class _TCalendarState extends State { right: 0, bottom: bottomOffset, child: ClipRect( - child: ValueListenableBuilder( - valueListenable: widget.bottomExpanded!, - builder: (context, expanded, child) { - return AnimatedSlide( - duration: _animDuration, - curve: _animCurve, - offset: expanded ? Offset.zero : const Offset(0, 1), - child: child, - ); - }, + child: AnimatedSlide( + duration: _animDuration, + curve: _animCurve, + offset: bottomExpanded ? Offset.zero : const Offset(0, 1), child: content, ), ), @@ -494,7 +510,10 @@ class _TCalendarState extends State { final btnPadding = widget.useSafeArea == true ? TTheme.of(context).spacer16 : TTheme.of(context).spacer16 * 2; - return safeBottom + btnPadding + _confirmBtnHeight; + // 仅默认确认按钮使用固定高度;自定义 confirmBtn 由调用方保证 bottom 不重叠。 + final btnHeight = + inherited?.confirmBtn == null ? _confirmBtnHeight : 0.0; + return safeBottom + btnPadding + btnHeight; } return safeBottom; diff --git a/tdesign-component/test/t_calendar_test.dart b/tdesign-component/test/t_calendar_test.dart new file mode 100644 index 000000000..5d9418dc4 --- /dev/null +++ b/tdesign-component/test/t_calendar_test.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +Widget _buildTestApp(Widget child) { + return TTheme( + data: TThemeData.defaultData(), + child: MaterialApp(home: Scaffold(body: child)), + ); +} + +void main() { + group('TCalendar — bottom / bottomExpanded', () { + test('bottomExpanded 未配合 bottom 时触发 assert', () { + expect( + () => TCalendar(bottomExpanded: ValueNotifier(false)), + throwsAssertionError, + ); + }); + + testWidgets('非 popup 使用 bottom 触发 assert', (tester) async { + await tester.pumpWidget( + _buildTestApp( + TCalendar( + title: '测试', + height: 640, + bottom: (_, __) => const Text('底部'), + ), + ), + ); + + expect(tester.takeException(), isA()); + expect(find.text('底部'), findsNothing); + }); + + testWidgets('popup 内选中变化时 bottom 会重建', (tester) async { + final day = DateTime(2024, 6, 15); + final dayMs = + DateTime(day.year, day.month, day.day).millisecondsSinceEpoch; + final selected = ValueNotifier>([dayMs]); + + await tester.pumpWidget( + _buildTestApp( + TCalendarInherited( + selected: selected, + usePopup: true, + child: TCalendar( + title: '测试', + height: 640, + value: selected.value, + bottom: (_, dates) => Text('days:${dates.length}'), + ), + ), + ), + ); + + expect(find.text('days:1'), findsOneWidget); + + final day2 = day.add(const Duration(days: 1)); + selected.value = [ + dayMs, + DateTime(day2.year, day2.month, day2.day).millisecondsSinceEpoch, + ]; + await tester.pump(); + + expect(find.text('days:2'), findsOneWidget); + }); + + testWidgets('popup 内 bottomExpanded 为 false 时处于收起偏移', (tester) async { + final expanded = ValueNotifier(false); + final selected = ValueNotifier>([]); + + await tester.pumpWidget( + _buildTestApp( + TCalendarInherited( + selected: selected, + usePopup: true, + child: TCalendar( + title: '测试', + height: 640, + bottomExpanded: expanded, + bottom: (_, __) => const Text('底部内容'), + ), + ), + ), + ); + await tester.pump(); + + var slide = tester.widget( + find.descendant( + of: find.byType(TCalendar), + matching: find.byType(SlideTransition), + ), + ); + expect(slide.position.value.dy, 1.0); + + expanded.value = true; + await tester.pump(); + await tester.pump(const Duration(milliseconds: 250)); + + slide = tester.widget( + find.descendant( + of: find.byType(TCalendar), + matching: find.byType(SlideTransition), + ), + ); + expect(slide.position.value.dy, 0.0); + }); + }); + + group('TCalendarPopup', () { + testWidgets('已有弹窗时再次 show 不会叠加', (tester) async { + late BuildContext context; + await tester.pumpWidget( + _buildTestApp( + Builder( + builder: (ctx) { + context = ctx; + return const SizedBox.shrink(); + }, + ), + ), + ); + + TCalendarPopup( + context, + child: const TCalendar(title: '日历A', height: 400), + ).show(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 300)); + + expect( + () => TCalendarPopup( + context, + child: const TCalendar(title: '日历B', height: 400), + ).show(), + throwsAssertionError, + ); + await tester.pump(); + + expect(find.text('日历A'), findsOneWidget); + expect(find.text('日历B'), findsNothing); + }); + }); +} From 249c93f2eccc1dc2830e5af9281521623e71f8e4 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 15:47:22 +0000 Subject: [PATCH 10/35] [autofix.ci] apply automated fixes --- tdesign-component/example/assets/api/calendar_api.md | 2 +- tdesign-site/src/calendar/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md index 4a1211271..0e8cad39b 100644 --- a/tdesign-component/example/assets/api/calendar_api.md +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -7,7 +7,7 @@ | anchorDate | DateTime? | - | 锚点日期 | | animateTo | bool? | false | 动画滚动到指定位置 | | bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,以浮层方式叠加在日历主体之上。 | -| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。 | +| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。**仅能在 [TCalendarPopup] 内使用。** | | cellHeight | double? | 60 | 日期高度 | | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | | dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 | diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md index 51cce744e..175cabc59 100644 --- a/tdesign-site/src/calendar/README.md +++ b/tdesign-site/src/calendar/README.md @@ -1252,7 +1252,7 @@ Widget _buildLunar(BuildContext context) { | anchorDate | DateTime? | - | 锚点日期 | | animateTo | bool? | false | 动画滚动到指定位置 | | bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,以浮层方式叠加在日历主体之上。 | -| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。 | +| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。**仅能在 [TCalendarPopup] 内使用。** | | cellHeight | double? | 60 | 日期高度 | | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | | dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 | From 89d67525625ee7e5fc469c138144b148611a4303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Mon, 18 May 2026 11:36:15 +0800 Subject: [PATCH 11/35] =?UTF-8?q?fix(calendar):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=97=A5=E6=9C=9F=E6=97=B6=E9=97=B4=E9=80=89=E6=8B=A9=E5=99=A8?= =?UTF-8?q?=E5=B9=B6=E9=87=8D=E6=9E=84=E6=97=A5=E5=8E=86=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/lib/page/t_calendar_page.dart | 295 +++++--- .../date_time_picker/t_date_time_picker.dart | 294 ++++++++ .../t_date_time_picker_model.dart | 707 ++++++++++++++++++ tdesign-component/lib/tdesign_flutter.dart | 1 + 4 files changed, 1196 insertions(+), 101 deletions(-) create mode 100644 tdesign-component/lib/src/components/date_time_picker/t_date_time_picker.dart create mode 100644 tdesign-component/lib/src/components/date_time_picker/t_date_time_picker_model.dart diff --git a/tdesign-component/example/lib/page/t_calendar_page.dart b/tdesign-component/example/lib/page/t_calendar_page.dart index 6b22a34f4..5c7841503 100644 --- a/tdesign-component/example/lib/page/t_calendar_page.dart +++ b/tdesign-component/example/lib/page/t_calendar_page.dart @@ -124,13 +124,13 @@ class _SingleCalendarCellState extends State<_SingleCalendarCell> { visible: true, onConfirm: (value) => setState(() => _selected = value), onClose: () => _expanded.value = false, - child: TCalendar( + builder: (ctx) => TCalendar( title: '请选择日期', value: _selected, height: size.height * 0.6 + 176, bottomExpanded: _expanded, onCellClick: (value, type, tdate) => _expanded.value = true, - bottom: (ctx, dates) { + bottom: (bCtx, dates) { final d = dates.isEmpty ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(dates.first); @@ -165,14 +165,14 @@ class _MultipleCalendarCellState extends State<_MultipleCalendarCell> { context, visible: true, onConfirm: (value) => setState(() => _dates = value), - child: TCalendar( + builder: (ctx) => TCalendar( title: '请选择日期', type: CalendarType.multiple, value: _dates.isEmpty ? [DateTime.now().millisecondsSinceEpoch] : _dates, height: size.height * 0.6 + 176, - bottom: (ctx, dates) => _MultipleSummary(selected: dates), + bottom: (bCtx, dates) => _MultipleSummary(selected: dates), ), ); }, @@ -207,12 +207,12 @@ class _RangeCalendarCellState extends State<_RangeCalendarCell> { context, visible: true, onConfirm: (value) => setState(() => _dates = value), - child: TCalendar( + builder: (ctx) => TCalendar( title: '请选择日期区间', type: CalendarType.range, value: _dates, height: size.height * 0.6 + 176, - bottom: (ctx, dates) => _RangeSummary(selected: dates), + bottom: (bCtx, dates) => _RangeSummary(selected: dates), ), ); }, @@ -229,23 +229,24 @@ class _SingleTimeCalendarCell extends StatefulWidget { } class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> { - late List _selected = [ - DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000, - ]; - late final ValueNotifier> _pickedTime; - late final TPickerColumns _timeItems = _buildTimeItems(); + late List _selected; + // 当前选中的时分(持久跨弹窗) + late int _hour; + late int _minute; @override void initState() { super.initState(); final now = DateTime.now(); - _pickedTime = ValueNotifier>([now.hour, now.minute]); - } - - @override - void dispose() { - _pickedTime.dispose(); - super.dispose(); + _hour = now.hour; + _minute = now.minute; + // 初始值就带上当前时分 + _selected = [ + DateTime.now() + .add(const Duration(days: 30)) + .copyWith(hour: _hour, minute: _minute, second: 0, millisecond: 0) + .millisecondsSinceEpoch, + ]; } @override @@ -255,10 +256,14 @@ class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> { return TCell( title: '单个选择日历和时间', arrow: true, - note: '${d.year}-${d.month}-${d.day} ' + note: '${d.year}-${d.month.toString().padLeft(2, '0')}-' + '${d.day.toString().padLeft(2, '0')} ' '${d.hour.toString().padLeft(2, '0')}:' '${d.minute.toString().padLeft(2, '0')}', onClick: (_) { + // 弹窗内的时分状态,生命周期与弹窗绑定 + var popupHour = _hour; + var popupMinute = _minute; TCalendarPopup( context, visible: true, @@ -266,22 +271,31 @@ class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> { final merged = dates.map((ms) { return DateTime.fromMillisecondsSinceEpoch(ms) .copyWith( - hour: _pickedTime.value[0], - minute: _pickedTime.value[1], + hour: popupHour, + minute: popupMinute, + second: 0, + millisecond: 0, ) .millisecondsSinceEpoch; }).toList(); - setState(() => _selected = merged); + setState(() { + _selected = merged; + _hour = popupHour; + _minute = popupMinute; + }); }, - child: TCalendar( + builder: (ctx) => TCalendar( title: '请选择日期和时间', value: _selected, height: size.height * 0.92, - bottom: (ctx, _) => _TimePickerPanel( - items: _timeItems, - initialValue: _pickedTime.value, + bottom: (_, __) => _TimePickerPanel( + initialHour: popupHour, + initialMinute: popupMinute, title: '选择时间', - onChange: (v) => _pickedTime.value = v, + onChange: (h, m) { + popupHour = h; + popupMinute = m; + }, ), ), ); @@ -298,18 +312,24 @@ class _RangeTimeCalendarCell extends StatefulWidget { } class _RangeTimeCalendarCellState extends State<_RangeTimeCalendarCell> { - List _dates = const []; - late List> _pickedRangeTime; - int _currentTab = 0; - late final TPickerColumns _timeItems = _buildTimeItems(); + late List _dates; + // 持久跨弹窗的开始/结束时分 + late List _startTime; + late List _endTime; @override void initState() { super.initState(); final now = DateTime.now(); - _pickedRangeTime = [ - [now.hour, now.minute], - [now.hour, now.minute], + _startTime = [now.hour, now.minute]; + _endTime = [now.hour, now.minute]; + // 初始值就带上当前时分 + _dates = [ + now.copyWith(second: 0, millisecond: 0).millisecondsSinceEpoch, + now + .add(const Duration(days: 3)) + .copyWith(second: 0, millisecond: 0) + .millisecondsSinceEpoch, ]; } @@ -323,43 +343,70 @@ class _RangeTimeCalendarCellState extends State<_RangeTimeCalendarCell> { ? '${_formatMdHm(_dates.first)} ~ ${_formatMdHm(_dates.last)}' : '--', onClick: (_) { + // 弹窗内的时分状态,生命周期与弹窗绑定 + var popupStartTime = List.from(_startTime); + var popupEndTime = List.from(_endTime); + // GlobalKey 在 onClick 作用域内创建,与弹窗生命周期一致 + final panelKey = GlobalKey<_RangeTimePickerPanelState>(); TCalendarPopup( context, visible: true, onConfirm: (value) { + if (value.length < 2) { + return; + } final merged = [ - for (var i = 0; i < value.length; i++) - DateTime.fromMillisecondsSinceEpoch(value[i]) - .copyWith( - hour: _pickedRangeTime[i][0], - minute: _pickedRangeTime[i][1], - ) - .millisecondsSinceEpoch, + DateTime.fromMillisecondsSinceEpoch(value[0]) + .copyWith( + hour: popupStartTime[0], + minute: popupStartTime[1], + second: 0, + millisecond: 0, + ) + .millisecondsSinceEpoch, + DateTime.fromMillisecondsSinceEpoch(value[1]) + .copyWith( + hour: popupEndTime[0], + minute: popupEndTime[1], + second: 0, + millisecond: 0, + ) + .millisecondsSinceEpoch, ]; - setState(() => _dates = merged); + setState(() { + _dates = merged; + _startTime = popupStartTime; + _endTime = popupEndTime; + }); }, - child: TCalendar( - title: '请选择日期和时间区间', - height: size.height * 0.92, - type: CalendarType.range, - value: _dates.isEmpty - ? [ - DateTime.now().millisecondsSinceEpoch, - DateTime.now() - .add(const Duration(days: 3)) - .millisecondsSinceEpoch, - ] - : _dates, - bottom: (ctx, _) => _RangeTimePickerPanel( - items: _timeItems, - currentTab: _currentTab, - initialValues: _pickedRangeTime, - onTabChanged: (tab) => _currentTab = tab, - onPickerChanged: (tab, value) { - _pickedRangeTime[tab] = value; + builder: (ctx) { + return TCalendar( + title: '请选择日期和时间区间', + height: size.height * 0.92, + type: CalendarType.range, + value: _dates, + // 点击开始日期 → 切到「开始时间」tab;点击结束日期 → 切到「结束时间」tab + onCellClick: (value, type, tdate) { + if (type == DateSelectType.start) { + panelKey.currentState?.switchTab(0); + } else if (type == DateSelectType.end) { + panelKey.currentState?.switchTab(1); + } }, - ), - ), + bottom: (_, __) => _RangeTimePickerPanel( + key: panelKey, + initialStartTime: popupStartTime, + initialEndTime: popupEndTime, + onChanged: (isStart, h, m) { + if (isStart) { + popupStartTime = [h, m]; + } else { + popupEndTime = [h, m]; + } + }, + ), + ); + }, ); }, ); @@ -390,7 +437,7 @@ class _AnchorCalendarCellState extends State<_AnchorCalendarCell> { context, visible: true, onConfirm: (dates) => setState(() => _selected = dates), - child: TCalendar( + builder: (ctx) => TCalendar( title: '请选择日期', minDate: DateTime(2022, 1, 1).millisecondsSinceEpoch, maxDate: DateTime(2028, 2, 15).millisecondsSinceEpoch, @@ -422,7 +469,8 @@ String _formatYmd(List dates) { return '--'; } final d = DateTime.fromMillisecondsSinceEpoch(dates.first); - return '${d.year}-${d.month}-${d.day}'; + return '${d.year}-${d.month.toString().padLeft(2, '0')}-' + '${d.day.toString().padLeft(2, '0')}'; } String _formatMd(int ms) { @@ -676,71 +724,106 @@ class _RangeSegment extends StatelessWidget { } } -class _TimePickerPanel extends StatelessWidget { +class _TimePickerPanel extends StatefulWidget { const _TimePickerPanel({ - required this.items, - required this.initialValue, + required this.initialHour, + required this.initialMinute, required this.title, required this.onChange, }); - final TPickerColumns items; - final List initialValue; + final int initialHour; + final int initialMinute; final String title; - final ValueChanged> onChange; + final void Function(int hour, int minute) onChange; + + @override + State<_TimePickerPanel> createState() => _TimePickerPanelState(); +} + +class _TimePickerPanelState extends State<_TimePickerPanel> { + late final TPickerColumns _items; + late final List _initialValue; + + @override + void initState() { + super.initState(); + _items = _buildTimeItems(); + _initialValue = [widget.initialHour, widget.initialMinute]; + } @override Widget build(BuildContext context) { return Container( decoration: _bottomCardDecoration(context), child: TPicker( - items: items, - initialValue: initialValue, + items: _items, + initialValue: _initialValue, height: 180, itemCount: 5, - title: title, - onChange: (v) => onChange(List.from(v.values)), + title: widget.title, + onChange: (v) { + final values = v.values; + if (values.length >= 2 && values[0] is int && values[1] is int) { + widget.onChange(values[0] as int, values[1] as int); + } + }, ), ); } } /// 区间+时间 demo 的双时间选择器面板(Tab 切换开始/结束) -/// 使用回调模式:子组件不持有 ValueNotifier,数据由父管理。 class _RangeTimePickerPanel extends StatefulWidget { const _RangeTimePickerPanel({ - required this.items, - required this.currentTab, - required this.initialValues, - required this.onTabChanged, - required this.onPickerChanged, + super.key, + required this.initialStartTime, + required this.initialEndTime, + required this.onChanged, }); - final TPickerColumns items; - final int currentTab; - final List> initialValues; - final ValueChanged onTabChanged; - final void Function(int tab, List value) onPickerChanged; + final List initialStartTime; + final List initialEndTime; + final void Function(bool isStart, int hour, int minute) onChanged; @override State<_RangeTimePickerPanel> createState() => _RangeTimePickerPanelState(); } -class _RangeTimePickerPanelState extends State<_RangeTimePickerPanel> { - late int _tab; +class _RangeTimePickerPanelState extends State<_RangeTimePickerPanel> + with SingleTickerProviderStateMixin { + late final TPickerColumns _items; + late final TabController _tabController; + int _tab = 0; + // 缓存用户在每个 tab 上最后选择的时分,不依赖 widget props 重置 + late List _startSelected; + late List _endSelected; @override void initState() { super.initState(); - _tab = widget.currentTab; + _items = _buildTimeItems(); + _tabController = TabController(length: 2, vsync: this); + _startSelected = List.from(widget.initialStartTime); + _endSelected = List.from(widget.initialEndTime); } @override - void didUpdateWidget(covariant _RangeTimePickerPanel oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.currentTab != widget.currentTab) { - _tab = widget.currentTab; + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + List get _currentInitialValue => + _tab == 0 ? _startSelected : _endSelected; + + /// 由外部(日历 onCellClick)驱动切换 tab + void switchTab(int tab) { + if (_tab == tab) { + return; } + setState(() => _tab = tab); + _tabController.animateTo(tab); } @override @@ -753,23 +836,33 @@ class _RangeTimePickerPanelState extends State<_RangeTimePickerPanel> { TTabBar( height: 40, showIndicator: true, + controller: _tabController, tabs: const [ TTab(text: '开始时间'), TTab(text: '结束时间'), ], - onTap: (i) { - setState(() => _tab = i); - widget.onTabChanged(i); - }, + onTap: (i) => setState(() => _tab = i), ), TPicker( key: ValueKey(_tab), - items: widget.items, - initialValue: widget.initialValues[_tab], + items: _items, + initialValue: _currentInitialValue, height: 180, itemCount: 5, - onChange: (v) => - widget.onPickerChanged(_tab, List.from(v.values)), + onChange: (v) { + final values = v.values; + if (values.length >= 2 && values[0] is int && values[1] is int) { + final h = values[0] as int; + final m = values[1] as int; + // 缓存当前 tab 的选中值,切回 tab 时恢复位置 + if (_tab == 0) { + _startSelected = [h, m]; + } else { + _endSelected = [h, m]; + } + widget.onChanged(_tab == 0, h, m); + } + }, ), ], ), diff --git a/tdesign-component/lib/src/components/date_time_picker/t_date_time_picker.dart b/tdesign-component/lib/src/components/date_time_picker/t_date_time_picker.dart new file mode 100644 index 000000000..e2fdc6e48 --- /dev/null +++ b/tdesign-component/lib/src/components/date_time_picker/t_date_time_picker.dart @@ -0,0 +1,294 @@ +import 'package:flutter/material.dart'; + +import '../../../tdesign_flutter.dart'; + +export 't_date_time_picker_model.dart'; + +/// 时间选择器 +/// +/// 基于 `TPicker` 的组合组件,仅负责日期/时间数据的列生成和回调转换, +/// 所有 UI 渲染和滚轮交互完全委托给 `TPicker`。 +/// +/// 通过 [mode] 控制列结构,支持快捷模式和数组精确模式。 +/// +/// 快捷模式: +/// ```dart +/// TDateTimePicker( +/// mode: DateTimePickerMode.date, +/// onConfirm: (v) => print(v.year), // 2026 +/// ) +/// ``` +/// +/// 数组模式(年月日 + 时分): +/// ```dart +/// TDateTimePicker( +/// mode: DateTimePickerMode.array(['date', 'minute']), +/// onConfirm: (v) => print(v.toDateTime()), +/// ) +/// ``` +/// +/// 附带星期列: +/// ```dart +/// TDateTimePicker( +/// mode: DateTimePickerMode.array(['date', 'week']), +/// onConfirm: (v) { +/// print(v.day); // 16 +/// print(v.week); // 1(周一) +/// // week 列为只读显示,随 day 变化自动联动,不可独立滚动选择 +/// }, +/// ) +/// ``` +class TDateTimePicker extends StatefulWidget { + const TDateTimePicker({ + super.key, + required this.mode, + this.format, + this.start, + this.end, + this.initialValue, + this.onCancel, + this.onChange, + this.onConfirm, + this.title, + this.titleWidget, + this.cancel, + this.confirm, + this.height, + this.itemCount, + }); + + /// 列结构模式(必填) + /// + /// 决定显示哪些列(年/月/日/时/分/秒/星期),参见 [DateTimePickerMode]。 + final DateTimePickerMode mode; + + /// 自定义列显示文案 + /// + /// 仅影响显示([TPickerOption.label]),不影响回调数据。 + /// 传入 `null` 时使用默认格式(数字 + 中文单位,如 "2026年"、"5月")。 + /// + /// ```dart + /// format: (column, value) { + /// if (column == DateTimeColumn.month) { + /// return value.toString().padLeft(2, '0'); + /// } + /// return '$value'; + /// } + /// ``` + final String Function(DateTimeColumn column, int value)? format; + + /// 可选范围起始日期 + /// + /// 当传 `null` 时,年列默认起始为「当前年 - + /// [DateTimePickerDataHelper.defaultYearOffset]」(默认 10 年前)。 + /// + /// 月、日、时、分、秒列在跨年/跨月时不会被裁剪。 + final DateTime? start; + + /// 可选范围结束日期 + /// + /// 当传 `null` 时,年列默认结束为「当前年 + + /// [DateTimePickerDataHelper.defaultYearOffset]」(默认 10 年后)。 + /// + /// 月、日、时、分、秒列在跨年/跨月时不会被裁剪。 + final DateTime? end; + + /// 初始选中值 + final DateTime? initialValue; + + /// 点击取消时触发 + final VoidCallback? onCancel; + + /// 选中值变化回调(滚动实时触发) + /// + /// 回调参数为 [TDateTimePickerValue],仅包含当前 [mode] 下涉及的列, + /// 其余字段为 `null`。 + final void Function(TDateTimePickerValue result)? onChange; + + /// 点击确认时触发 + /// + /// 回调参数为 [TDateTimePickerValue],仅包含当前 [mode] 下涉及的列, + /// 其余字段为 `null`。 + /// + /// 通过 [TDateTimePickerValue.toDateTime] 可将结果重组为 `DateTime`: + /// ```dart + /// onConfirm: (v) { + /// final dt = v.toDateTime(); + /// }, + /// ``` + final void Function(TDateTimePickerValue result)? onConfirm; + + /// 工具栏标题文字 + final String? title; + + /// 自定义标题组件(优先级高于 [title]) + final Widget? titleWidget; + + /// 工具栏左侧自定义插槽 + final Widget? cancel; + + /// 工具栏右侧自定义插槽 + final Widget? confirm; + + /// 面板视窗高度 + final double? height; + + /// 每屏显示条目数量 + final int? itemCount; + + @override + State createState() => _TDateTimePickerState(); +} + +class _TDateTimePickerState extends State { + late List _columns; + late DateTime _current; + late TPickerColumns _pickerColumns; + late List _initialValue; + + /// 上一次 onChange 的 values(用于判断年/月是否变化) + List _lastValues = const []; + + @override + void initState() { + super.initState(); + _columns = widget.mode.columns; + _current = widget.initialValue ?? DateTime.now(); + _pickerColumns = _buildPickerColumns(); + _initialValue = DateTimePickerDataHelper.buildInitialValue( + columns: _columns, + value: _current, + ); + _lastValues = List.of(_initialValue); + } + + @override + void didUpdateWidget(covariant TDateTimePicker oldWidget) { + super.didUpdateWidget(oldWidget); + final modeChanged = !_columnsEqual( + oldWidget.mode.columns, + widget.mode.columns, + ); + final initialValueChanged = oldWidget.initialValue != widget.initialValue; + final rangeChanged = + oldWidget.start != widget.start || oldWidget.end != widget.end; + final formatChanged = oldWidget.format != widget.format; + + if (!modeChanged && !initialValueChanged && !rangeChanged && !formatChanged) { + return; + } + + if (modeChanged) { + _columns = widget.mode.columns; + } + if (modeChanged || initialValueChanged) { + _current = widget.initialValue ?? DateTime.now(); + _initialValue = DateTimePickerDataHelper.buildInitialValue( + columns: _columns, + value: _current, + ); + _lastValues = List.of(_initialValue); + } + _pickerColumns = _buildPickerColumns(); + } + + TPickerColumns _buildPickerColumns() { + return DateTimePickerDataHelper.buildColumns( + columns: _columns, + start: widget.start, + end: widget.end, + current: _current, + format: widget.format, + ); + } + + TDateTimePickerValue _toResult(TPickerValue pickerValue) { + return DateTimePickerDataHelper.toResult( + columns: _columns, + values: pickerValue.values, + ); + } + + void _handleChange(TPickerValue pickerValue) { + final newValues = pickerValue.values; + + // 检查是否需要联动刷新(年/月变化 → 日列天数变化;日变化 → 星期变化) + if (DateTimePickerDataHelper.needsRefresh( + _columns, + _lastValues, + newValues, + ) || + _needsWeekRefresh(newValues)) { + _current = DateTimePickerDataHelper.resolveCurrentDateTime( + columns: _columns, + values: newValues, + fallback: _current, + ); + setState(() { + _pickerColumns = _buildPickerColumns(); + // 更新 initialValue 以保持 TPicker 的选中位置 + _initialValue = DateTimePickerDataHelper.buildInitialValue( + columns: _columns, + value: _current, + ); + }); + } + + _lastValues = List.of(newValues); + widget.onChange?.call(_toResult(pickerValue)); + } + + /// 检查是否需要刷新星期列(日列变化时星期也需要更新) + bool _needsWeekRefresh(List newValues) { + if (!_columns.contains(DateTimeColumn.week)) { + return false; + } + for (var i = 0; i < _columns.length && i < _lastValues.length && i < newValues.length; i++) { + final col = _columns[i]; + if ((col == DateTimeColumn.year || + col == DateTimeColumn.month || + col == DateTimeColumn.day) && + _lastValues[i] != newValues[i]) { + return true; + } + } + return false; + } + + void _handleConfirm(TPickerValue pickerValue) { + widget.onConfirm?.call(_toResult(pickerValue)); + } + + @override + Widget build(BuildContext context) { + return TPicker( + items: _pickerColumns, + initialValue: _initialValue, + title: widget.title, + titleWidget: widget.titleWidget, + cancel: widget.cancel, + confirm: widget.confirm, + height: widget.height ?? 200, + itemCount: widget.itemCount ?? 5, + onCancel: widget.onCancel, + onChange: _handleChange, + onConfirm: _handleConfirm, + ); + } + + static bool _columnsEqual( + List a, List b) { + if (identical(a, b)) { + return true; + } + if (a.length != b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; + } +} diff --git a/tdesign-component/lib/src/components/date_time_picker/t_date_time_picker_model.dart b/tdesign-component/lib/src/components/date_time_picker/t_date_time_picker_model.dart new file mode 100644 index 000000000..a23144fb1 --- /dev/null +++ b/tdesign-component/lib/src/components/date_time_picker/t_date_time_picker_model.dart @@ -0,0 +1,707 @@ +import 'package:flutter/foundation.dart'; + +import '../picker/t_picker_items.dart'; +import '../picker/t_picker_option.dart'; + +/// 日期时间列类型枚举 +enum DateTimeColumn { + year, + month, + day, + hour, + minute, + second, + week, +} + +/// 时间选择器模式 +/// +/// 支持两种用法: +/// +/// 1. **快捷模式**(字符串)— 自动包含从年到指定粒度的所有列: +/// - `DateTimePickerMode.year` → 年 +/// - `DateTimePickerMode.month` → 年月 +/// - `DateTimePickerMode.date` → 年月日 +/// - `DateTimePickerMode.hour` → 年月日时 +/// - `DateTimePickerMode.minute` → 年月日时分 +/// - `DateTimePickerMode.second` → 年月日时分秒 +/// +/// 2. **数组模式** — 精确控制显示哪些列: +/// - 第一个值控制日期粒度(year/month/date),第二个值控制时间粒度(hour/minute/second) +/// - 支持附加 `week` 列:`DateTimePickerMode.array(['date', 'week'])` +/// +/// 示例: +/// ```dart +/// // 快捷:年月日 +/// DateTimePickerMode.date +/// +/// // 数组:年月日 + 时分 +/// DateTimePickerMode.array(['date', 'minute']) +/// +/// // 数组:年月日 + 星期 +/// DateTimePickerMode.array(['date', 'week']) +/// ``` +sealed class DateTimePickerMode { + const DateTimePickerMode(); + + /// 仅显示年 + static const year = ShortcutMode._('year'); + + /// 年月 + static const month = ShortcutMode._('month'); + + /// 年月日 + static const date = ShortcutMode._('date'); + + /// 年月日时 + static const hour = ShortcutMode._('hour'); + + /// 年月日时分 + static const minute = ShortcutMode._('minute'); + + /// 年月日时分秒 + static const second = ShortcutMode._('second'); + + /// 数组模式:精确控制列组合 + const factory DateTimePickerMode.array(List values) = ArrayMode; + + /// 解析 mode 为列类型列表 + List get columns; +} + +/// 快捷模式:按粒度自动包含从年到指定类型的所有列 +class ShortcutMode extends DateTimePickerMode { + const ShortcutMode._(this.value); + + final String value; + + static const _shortcutColumns = >{ + 'year': [DateTimeColumn.year], + 'month': [DateTimeColumn.year, DateTimeColumn.month], + 'date': [DateTimeColumn.year, DateTimeColumn.month, DateTimeColumn.day], + 'hour': [ + DateTimeColumn.year, + DateTimeColumn.month, + DateTimeColumn.day, + DateTimeColumn.hour, + ], + 'minute': [ + DateTimeColumn.year, + DateTimeColumn.month, + DateTimeColumn.day, + DateTimeColumn.hour, + DateTimeColumn.minute, + ], + 'second': [ + DateTimeColumn.year, + DateTimeColumn.month, + DateTimeColumn.day, + DateTimeColumn.hour, + DateTimeColumn.minute, + DateTimeColumn.second, + ], + }; + + @override + List get columns { + // 私有构造保证 value 必为 _shortcutColumns 中的合法 key + final cols = _shortcutColumns[value]; + assert(cols != null, 'ShortcutMode 未知 value: "$value",必须使用 DateTimePickerMode 提供的静态常量'); + return cols!; + } +} + +/// 数组模式:精确控制列组合 +/// +/// - 第一个值控制日期粒度(year/month/date) +/// - 第二个值控制时间粒度(hour/minute/second) +/// - 可附加 `week` 列 +class ArrayMode extends DateTimePickerMode { + const ArrayMode(this.values); + + final List values; + + /// 合法值集合 + static const _validValues = { + 'year', + 'month', + 'date', + 'hour', + 'minute', + 'second', + 'week', + }; + + static const _dateColumns = >{ + 'year': [DateTimeColumn.year], + 'month': [DateTimeColumn.year, DateTimeColumn.month], + 'date': [DateTimeColumn.year, DateTimeColumn.month, DateTimeColumn.day], + }; + + static const _timeColumns = >{ + 'hour': [DateTimeColumn.hour], + 'minute': [DateTimeColumn.hour, DateTimeColumn.minute], + 'second': [ + DateTimeColumn.hour, + DateTimeColumn.minute, + DateTimeColumn.second, + ], + }; + + @override + List get columns { + assert(values.isNotEmpty, 'ArrayMode.values 不能为空'); + final result = []; + final seen = {}; + for (final v in values) { + assert( + _validValues.contains(v), + 'ArrayMode 不支持的值: "$v",合法值: $_validValues', + ); + List? cols; + if (_dateColumns.containsKey(v)) { + cols = _dateColumns[v]; + } else if (_timeColumns.containsKey(v)) { + cols = _timeColumns[v]; + } else if (v == 'week') { + cols = const [DateTimeColumn.week]; + } + if (cols == null) { + continue; + } + // 去重保持顺序 + for (final c in cols) { + if (seen.add(c)) { + result.add(c); + } + } + } + return result; + } +} + +/// 日期时间选择器的回调结果 +/// +/// 所有字段均为 **nullable**——字段为 `null` 表示当前 [DateTimePickerMode] 下 +/// 未包含该列,不是"用户选了 null"。 +/// +/// 示例: +/// ```dart +/// TDateTimePicker( +/// mode: DateTimePickerMode.date, +/// onConfirm: (v) { +/// print(v.year); // 2025 +/// print(v.month); // 6 +/// print(v.day); // 15 +/// print(v.hour); // null(date 模式不含时) +/// }, +/// ) +/// ``` +/// +/// 通过 [toDateTime] 可将结果重组为 `DateTime`: +/// ```dart +/// onConfirm: (v) { +/// final dt = v.toDateTime(); // DateTime(2025, 6, 15) +/// }, +/// ``` +@immutable +class TDateTimePickerValue { + const TDateTimePickerValue({ + this.year, + this.month, + this.day, + this.hour, + this.minute, + this.second, + this.week, + }); + + /// 年(模式包含 year 列时有值) + final int? year; + + /// 月(模式包含 month 列时有值) + final int? month; + + /// 日(模式包含 day 列时有值) + final int? day; + + /// 时(模式包含 hour 列时有值) + final int? hour; + + /// 分(模式包含 minute 列时有值) + final int? minute; + + /// 秒(模式包含 second 列时有值) + final int? second; + + /// 星期(模式包含 week 列时有值,1=周一 … 7=周日) + /// + /// 注意:week 列为只读显示,随 year/month/day 变化自动联动, + /// 其值等价于 [toDateTime]?.weekday,不可独立选择。 + final int? week; + + /// 将选中结果重组为 [DateTime] + /// + /// 缺失字段使用 [fallback] 对应字段填充(默认 [DateTime.now])。 + /// + /// ```dart + /// // mode = DateTimePickerMode.date + /// // value = TDateTimePickerValue(year: 2025, month: 6, day: 15) + /// value.toDateTime(); // DateTime(2025, 6, 15, 0, 0, 0) + /// ``` + DateTime toDateTime({DateTime? fallback}) { + final fb = fallback ?? DateTime.now(); + return DateTime( + year ?? fb.year, + month ?? fb.month, + day ?? fb.day, + hour ?? fb.hour, + minute ?? fb.minute, + second ?? fb.second, + ); + } + + /// 从 [DateTimeColumn] 列表和对应的 int 值列表构建结果 + factory TDateTimePickerValue.fromColumns({ + required List columns, + required List values, + }) { + int? year, month, day, hour, minute, second, week; + for (var i = 0; i < columns.length && i < values.length; i++) { + final v = values[i]; + if (v is! int) { + continue; + } + switch (columns[i]) { + case DateTimeColumn.year: + year = v; + case DateTimeColumn.month: + month = v; + case DateTimeColumn.day: + day = v; + case DateTimeColumn.hour: + hour = v; + case DateTimeColumn.minute: + minute = v; + case DateTimeColumn.second: + second = v; + case DateTimeColumn.week: + week = v; + } + } + return TDateTimePickerValue( + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + week: week, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TDateTimePickerValue && + year == other.year && + month == other.month && + day == other.day && + hour == other.hour && + minute == other.minute && + second == other.second && + week == other.week; + + @override + int get hashCode => Object.hash(year, month, day, hour, minute, second, week); + + @override + String toString() => + 'TDateTimePickerValue(year: $year, month: $month, day: $day, ' + 'hour: $hour, minute: $minute, second: $second, week: $week)'; +} + +/// 日期时间列数据生成器 +/// +/// 纯数据工具类,根据列类型列表、范围和格式化函数生成 `TPickerColumns`。 +/// 不管理任何 UI 状态(controller 等由 `TPicker` 内部处理)。 +abstract final class DateTimePickerDataHelper { + /// 默认年范围相对于当前年份的偏移(前后各 10 年) + static const int defaultYearOffset = 10; + + /// 计算默认起始年(当前年 - [defaultYearOffset]) + static int defaultStartYear([DateTime? now]) => + (now ?? DateTime.now()).year - defaultYearOffset; + + /// 计算默认结束年(当前年 + [defaultYearOffset]) + static int defaultEndYear([DateTime? now]) => + (now ?? DateTime.now()).year + defaultYearOffset; + + /// 默认单位 + static const Map defaultUnits = { + DateTimeColumn.year: '年', + DateTimeColumn.month: '月', + DateTimeColumn.day: '日', + DateTimeColumn.hour: '时', + DateTimeColumn.minute: '分', + DateTimeColumn.second: '秒', + DateTimeColumn.week: '', + }; + + /// 星期文案 + static const List weekLabels = [ + '周一', + '周二', + '周三', + '周四', + '周五', + '周六', + '周日', + ]; + + /// 根据列类型、范围和当前选中值生成 [TPickerColumns] + /// + /// [columns] 需要生成的列类型列表 + /// [start] 可选范围起始 + /// [end] 可选范围结束 + /// [current] 当前选中值(用于计算日列天数和星期列) + /// [format] 自定义格式化函数 + /// + /// 当 [start] 晚于 [end] 时:debug 模式下会触发 assert,release 模式下会忽略 [end], + /// 仅以 [start] 作为下界,避免崩溃。 + static TPickerColumns buildColumns({ + required List columns, + DateTime? start, + DateTime? end, + DateTime? current, + String Function(DateTimeColumn column, int value)? format, + }) { + assert( + start == null || end == null || !start.isAfter(end), + 'DateTimePickerDataHelper.buildColumns: start ($start) 不能晚于 end ($end)', + ); + // release 模式下若 start > end,丢弃 end 以避免空区间崩溃 + final safeEnd = (start != null && end != null && start.isAfter(end)) + ? null + : end; + final now = current ?? DateTime.now(); + final result = >[]; + + for (final col in columns) { + switch (col) { + case DateTimeColumn.year: + result.add(_buildYearColumn(start, safeEnd, now, format)); + case DateTimeColumn.month: + result.add(_buildMonthColumn(start, safeEnd, now, format)); + case DateTimeColumn.day: + result.add(_buildDayColumn(start, safeEnd, now, format)); + case DateTimeColumn.hour: + result.add(_buildHourColumn(start, safeEnd, now, format)); + case DateTimeColumn.minute: + result.add(_buildMinuteColumn(start, safeEnd, now, format)); + case DateTimeColumn.second: + result.add(_buildSecondColumn(start, safeEnd, now, format)); + case DateTimeColumn.week: + result.add(_buildWeekColumn(now, format)); + } + } + + return TPickerColumns(result); + } + + /// 根据列类型列表和当前选中值,计算 `TPicker` 的 initialValue + static List buildInitialValue({ + required List columns, + required DateTime value, + }) { + final result = []; + for (final col in columns) { + switch (col) { + case DateTimeColumn.year: + result.add(value.year); + case DateTimeColumn.month: + result.add(value.month); + case DateTimeColumn.day: + result.add(value.day); + case DateTimeColumn.hour: + result.add(value.hour); + case DateTimeColumn.minute: + result.add(value.minute); + case DateTimeColumn.second: + result.add(value.second); + case DateTimeColumn.week: + result.add(value.weekday); + } + } + return result; + } + + /// 将 TPicker 回调的 values 转为 [TDateTimePickerValue] + /// + /// 推荐直接使用 [TDateTimePickerValue.fromColumns],此方法为便利封装。 + static TDateTimePickerValue toResult({ + required List columns, + required List values, + }) { + return TDateTimePickerValue.fromColumns( + columns: columns, + values: values, + ); + } + + /// 从 TPicker 回调的 values 中恢复出当前选中的 DateTime + /// + /// 用于联动刷新时重新计算日列天数和星期列。 + static DateTime resolveCurrentDateTime({ + required List columns, + required List values, + DateTime? fallback, + }) { + final fb = fallback ?? DateTime.now(); + var year = fb.year; + var month = fb.month; + var day = fb.day; + var hour = fb.hour; + var minute = fb.minute; + var second = fb.second; + + for (var i = 0; i < columns.length && i < values.length; i++) { + final v = values[i]; + if (v is! int) { + continue; + } + switch (columns[i]) { + case DateTimeColumn.year: + year = v; + case DateTimeColumn.month: + month = v; + case DateTimeColumn.day: + day = v; + case DateTimeColumn.hour: + hour = v; + case DateTimeColumn.minute: + minute = v; + case DateTimeColumn.second: + second = v; + case DateTimeColumn.week: + break; + } + } + + // 修正日期(如 2 月 30 日 → 2 月 28/29 日) + final maxDay = _daysInMonth(year, month); + if (day > maxDay) { + day = maxDay; + } + + return DateTime(year, month, day, hour, minute, second); + } + + /// 检查年/月列是否发生变化(决定是否需要联动刷新) + static bool needsRefresh( + List columns, + List oldValues, + List newValues, + ) { + for (var i = 0; i < columns.length && i < oldValues.length && i < newValues.length; i++) { + final col = columns[i]; + if ((col == DateTimeColumn.year || col == DateTimeColumn.month) && + oldValues[i] != newValues[i]) { + return true; + } + } + return false; + } + + // ──────── 列数据生成 ──────── + + static List _buildYearColumn( + DateTime? start, + DateTime? end, + DateTime current, + String Function(DateTimeColumn, int)? format, + ) { + final startYear = start?.year ?? defaultStartYear(current); + final endYear = end?.year ?? defaultEndYear(current); + return [ + for (var y = startYear; y <= endYear; y++) + TPickerOption( + label: format?.call(DateTimeColumn.year, y) ?? + '$y${defaultUnits[DateTimeColumn.year]}', + value: y, + ), + ]; + } + + static List _buildMonthColumn( + DateTime? start, + DateTime? end, + DateTime current, + String Function(DateTimeColumn, int)? format, + ) { + var startMonth = 1; + var endMonth = 12; + + // 当前年 == 起始年时,裁剪起始月 + if (start != null && current.year == start.year) { + startMonth = start.month; + } + // 当前年 == 结束年时,裁剪结束月 + if (end != null && current.year == end.year) { + endMonth = end.month; + } + + return [ + for (var m = startMonth; m <= endMonth; m++) + TPickerOption( + label: format?.call(DateTimeColumn.month, m) ?? + '$m${defaultUnits[DateTimeColumn.month]}', + value: m, + ), + ]; + } + + static List _buildDayColumn( + DateTime? start, + DateTime? end, + DateTime current, + String Function(DateTimeColumn, int)? format, + ) { + final maxDay = _daysInMonth(current.year, current.month); + var startDay = 1; + var endDay = maxDay; + + if (start != null && + current.year == start.year && + current.month == start.month) { + startDay = start.day; + } + if (end != null && + current.year == end.year && + current.month == end.month) { + endDay = end.day.clamp(1, maxDay); + } + + return [ + for (var d = startDay; d <= endDay; d++) + TPickerOption( + label: format?.call(DateTimeColumn.day, d) ?? + '$d${defaultUnits[DateTimeColumn.day]}', + value: d, + ), + ]; + } + + static List _buildHourColumn( + DateTime? start, + DateTime? end, + DateTime current, + String Function(DateTimeColumn, int)? format, + ) { + var startHour = 0; + var endHour = 23; + + if (start != null && _isSameDate(current, start)) { + startHour = start.hour; + } + if (end != null && _isSameDate(current, end)) { + endHour = end.hour; + } + + return [ + for (var h = startHour; h <= endHour; h++) + TPickerOption( + label: format?.call(DateTimeColumn.hour, h) ?? + '$h${defaultUnits[DateTimeColumn.hour]}', + value: h, + ), + ]; + } + + static List _buildMinuteColumn( + DateTime? start, + DateTime? end, + DateTime current, + String Function(DateTimeColumn, int)? format, + ) { + var startMinute = 0; + var endMinute = 59; + + if (start != null && + _isSameDate(current, start) && + current.hour == start.hour) { + startMinute = start.minute; + } + if (end != null && + _isSameDate(current, end) && + current.hour == end.hour) { + endMinute = end.minute; + } + + return [ + for (var m = startMinute; m <= endMinute; m++) + TPickerOption( + label: format?.call(DateTimeColumn.minute, m) ?? + '$m${defaultUnits[DateTimeColumn.minute]}', + value: m, + ), + ]; + } + + static List _buildSecondColumn( + DateTime? start, + DateTime? end, + DateTime current, + String Function(DateTimeColumn, int)? format, + ) { + var startSecond = 0; + var endSecond = 59; + + if (start != null && + _isSameDate(current, start) && + current.hour == start.hour && + current.minute == start.minute) { + startSecond = start.second; + } + if (end != null && + _isSameDate(current, end) && + current.hour == end.hour && + current.minute == end.minute) { + endSecond = end.second; + } + + return [ + for (var s = startSecond; s <= endSecond; s++) + TPickerOption( + label: format?.call(DateTimeColumn.second, s) ?? + '$s${defaultUnits[DateTimeColumn.second]}', + value: s, + ), + ]; + } + + static List _buildWeekColumn( + DateTime current, + String Function(DateTimeColumn, int)? format, + ) { + // 星期列固定只显示当前日期对应的星期几 + final weekday = current.weekday; // 1=周一 ... 7=周日 + return [ + TPickerOption( + label: format?.call(DateTimeColumn.week, weekday) ?? + weekLabels[weekday - 1], + value: weekday, + ), + ]; + } + + // ──────── 工具方法 ──────── + + static int _daysInMonth(int year, int month) { + return DateTime(year, month + 1, 0).day; + } + + static bool _isSameDate(DateTime a, DateTime b) { + return a.year == b.year && a.month == b.month && a.day == b.day; + } +} diff --git a/tdesign-component/lib/tdesign_flutter.dart b/tdesign-component/lib/tdesign_flutter.dart index 82d5e6261..84848860e 100644 --- a/tdesign-component/lib/tdesign_flutter.dart +++ b/tdesign-component/lib/tdesign_flutter.dart @@ -45,6 +45,7 @@ export 'src/components/message/t_message.dart'; export 'src/components/navbar/t_nav_bar.dart'; export 'src/components/notice_bar/t_notice_bar.dart'; export 'src/components/notice_bar/t_notice_bar_style.dart'; +export 'src/components/date_time_picker/t_date_time_picker.dart'; export 'src/components/picker/t_item_widget.dart'; export 'src/components/picker/t_picker.dart'; export 'src/components/picker/t_picker_items.dart'; From def113510f054c33d8fa66275846f807408278e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Mon, 18 May 2026 14:26:14 +0800 Subject: [PATCH 12/35] =?UTF-8?q?refactor:=20=E8=B0=83=E6=95=B4=20TDateTim?= =?UTF-8?q?ePicker=20=E5=AF=BC=E5=87=BA=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tdesign-component/lib/tdesign_flutter.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tdesign-component/lib/tdesign_flutter.dart b/tdesign-component/lib/tdesign_flutter.dart index 84848860e..8a6b90375 100644 --- a/tdesign-component/lib/tdesign_flutter.dart +++ b/tdesign-component/lib/tdesign_flutter.dart @@ -16,7 +16,7 @@ export 'src/components/checkbox/t_check_box.dart'; export 'src/components/checkbox/t_check_box_group.dart'; export 'src/components/collapse/t_collapse.dart'; export 'src/components/collapse/t_collapse_panel.dart'; -export 'src/components/dialog/t_dialog.dart'; +export 'src/components/date_time_picker/t_date_time_picker.dart'; export 'src/components/divider/t_divider.dart'; export 'src/components/drawer/t_drawer.dart'; export 'src/components/drawer/t_drawer_widget.dart'; @@ -45,7 +45,6 @@ export 'src/components/message/t_message.dart'; export 'src/components/navbar/t_nav_bar.dart'; export 'src/components/notice_bar/t_notice_bar.dart'; export 'src/components/notice_bar/t_notice_bar_style.dart'; -export 'src/components/date_time_picker/t_date_time_picker.dart'; export 'src/components/picker/t_item_widget.dart'; export 'src/components/picker/t_picker.dart'; export 'src/components/picker/t_picker_items.dart'; From 37a6874e7ce37d2ac12b154dbf3f7252747e2739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Tue, 19 May 2026 14:36:34 +0800 Subject: [PATCH 13/35] =?UTF-8?q?feat(calendar):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=BC=B9=E7=AA=97API=E4=B8=BATCalendar.showPopup=E5=B9=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/assets/api/calendar_api.md | 72 ++- .../assets/code/calendar._buildBlock.txt | 8 +- .../assets/code/calendar._buildCustomCell.txt | 120 ---- .../assets/code/calendar._buildLunar.txt | 8 +- .../assets/code/calendar._buildStyle.txt | 215 ++++--- .../example/lib/page/t_calendar_page.dart | 607 ++++++++---------- .../src/components/calendar/t_calendar.dart | 333 +++++++++- .../components/calendar/t_calendar_body.dart | 253 +++++--- .../components/calendar/t_calendar_cell.dart | 7 +- .../components/calendar/t_calendar_popup.dart | 221 ------- .../components/calendar/t_calendar_style.dart | 1 + .../components/dialog/t_dialog_widget.dart | 1 + .../src/components/popup/t_popup_panel.dart | 87 +-- tdesign-component/lib/tdesign_flutter.dart | 1 + tdesign-component/test/t_calendar_test.dart | 342 +++++++++- 15 files changed, 1299 insertions(+), 977 deletions(-) delete mode 100644 tdesign-component/example/assets/code/calendar._buildCustomCell.txt delete mode 100644 tdesign-component/lib/src/components/calendar/t_calendar_popup.dart diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md index 0e8cad39b..4072b7969 100644 --- a/tdesign-component/example/assets/api/calendar_api.md +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -2,12 +2,14 @@ ### TCalendar #### 默认构造方法 +按照日历形式展示数据或日期的容器,支持内嵌和弹窗两种模式。 + | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | anchorDate | DateTime? | - | 锚点日期 | | animateTo | bool? | false | 动画滚动到指定位置 | -| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,以浮层方式叠加在日历主体之上。 | -| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。**仅能在 [TCalendarPopup] 内使用。** | +| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,以浮层方式叠加在日历主体之上。**仅能在 Popup 内使用。** | +| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。**仅能在 Popup 内使用。** | | cellHeight | double? | 60 | 日期高度 | | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | | dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 | @@ -17,8 +19,8 @@ | format | CalendarFormat? | - | 用于格式化日期的函数,可定义日期前后的显示内容和日期样式 | | height | double? | - | 高度 | | key | | - | | -| maxDate | int? | - | 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认半年后 | -| minDate | int? | - | 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认今天 | +| maxDate | int? | - | 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认 2100-12-31 | +| minDate | int? | - | 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认 1970-01-01 | | monthTitleBuilder | Widget Function(BuildContext context, DateTime monthDate)? | - | 月标题构建器 | | monthTitleHeight | double? | 22 | 月标题高度 | | onCellClick | void Function(int value, DateSelectType type, TDate tdate)? | - | 点击日期时触发 | @@ -38,20 +40,54 @@ ``` ``` -### TCalendarPopup -#### 默认构造方法 +### TCalendar.showPopup +#### 静态方法 + +弹出日历选择器,返回选中的日期列表。取消或关闭弹窗时返回 `null`;点击确认时返回选中日期的毫秒时间戳列表。 + +```dart +final result = await TCalendar.showPopup( + context, + title: '请选择日期', + type: CalendarType.single, +); +if (result != null) { + print('选中了: $result'); +} +``` + +若需完全自定义布局,请直接使用 `TCalendar` + `TPopupBottomDisplayPanel` + `TSlidePopupRoute` 自行组装。 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| autoClose | bool? | true | 是否在点击关闭按钮、确认按钮或遮罩层时自动关闭弹窗(默认 true) | -| builder | CalendarBuilder? | - | 日历构建器,优先级高于 [child] | -| child | TCalendar? | - | 日历控件,当 [builder] 为 null 时使用 | -| confirmBtn | Widget? | - | 自定义确认按钮;为 null 时使用默认主色 [TButton] | -| context | BuildContext | context | 触发 popup 时的根 context,用于 [Navigator.of] 查找并 push 弹窗路由 | -| onClose | VoidCallback? | - | 弹窗关闭后回调 | -| onConfirm | void Function(List value)? | - | 点击确认按钮时回调,参数为当前选中的日期时间戳列表(毫秒) | -| top | double? | - | 弹窗顶部距离屏幕顶部的偏移量 | -| visible | bool? | - | 是否在构造时立即调用 [show] 打开弹窗(默认 false) | +| context | BuildContext | - | 必填,触发 popup 时的 context | +| title | String? | - | 弹窗标题 | +| type | CalendarType | CalendarType.single | 日历选择类型 | +| value | List? | - | 当前选中的日期(毫秒时间戳列表) | +| minDate | int? | - | 最小可选日期(毫秒时间戳),不传则默认 1970-01-01 | +| maxDate | int? | - | 最大可选日期(毫秒时间戳),不传则默认 2100-12-31 | +| anchorDate | DateTime? | - | 锚点日期,弹出时滚动到该月 | +| fixedHeight | double? | - | 面板固定高度(不传时自动计算) | +| firstDayOfWeek | int? | 0 | 第一天从星期几开始,默认 0 = 周日 | +| displayFormat | String? | 'year month' | 年月显示格式 | +| cellHeight | double? | 60 | 日期高度 | +| style | TCalendarStyle? | - | 自定义样式 | +| format | CalendarFormat? | - | 用于格式化日期的函数 | +| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器 | +| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式) | +| confirmBtn | Widget? | - | 自定义确认按钮 | +| onConfirm | void Function(List)? | - | 点击确认的额外回调 | +| onClose | VoidCallback? | - | 弹窗关闭后的回调 | +| onCellClick | void Function(int value, DateSelectType type, TDate tdate)? | - | 点击日期时触发 | +| onCellLongPress | void Function(int value, DateSelectType type, TDate tdate)? | - | 长按日期时触发 | +| autoClose | bool | true | 点击遮罩或物理返回是否关闭 | +| draggable | bool | false | 面板是否可拖动 | +| cellWidget | Widget? Function(BuildContext, TDate, DateSelectType)? | - | 自定义日期单元格组件 | +| dateType | TCalendarDateType | TCalendarDateType.solar | 日历类型:阳历或农历 | +| dataSource | TCalendarDataSource? | - | 外部数据源 | +| showLunarInfo | bool | false | 阳历模式下是否显示农历信息 | +| onMonthChange | ValueChanged? | - | 月份变化时触发 | +| monthTitleBuilder | Widget Function(BuildContext, DateTime)? | - | 月标题构建器 | ``` ``` @@ -71,6 +107,7 @@ | titleCloseColor | Color? | - | header区域 关闭图标的颜色 | | titleMaxLine | int? | - | header区域 [TCalendar.title]的行数 | | titleStyle | TextStyle? | - | header区域 [TCalendar.title]的样式 | +| todayStyle | TextStyle? | - | 当天日期样式 | | weekdayStyle | TextStyle? | - | header区域 周 文字样式 | @@ -85,6 +122,11 @@ ``` ### TCalendarDataSource + +日历数据源抽象类,用于提供农历、节气、节日、假期等附加信息。 + +继承此类并实现相应方法即可为日历添加农历支持。参考 `LunarDataSourceExample`。 + ``` ``` diff --git a/tdesign-component/example/assets/code/calendar._buildBlock.txt b/tdesign-component/example/assets/code/calendar._buildBlock.txt index fa5987012..83e36a8fc 100644 --- a/tdesign-component/example/assets/code/calendar._buildBlock.txt +++ b/tdesign-component/example/assets/code/calendar._buildBlock.txt @@ -1,15 +1,12 @@ Widget _buildBlock(BuildContext context) { - final size = MediaQuery.of(context).size; final selected = ValueNotifier>( [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000], ); return Column( - // spacing: TTheme.of(context).spacer16, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - // spacing: TTheme.of(context).spacer16, mainAxisAlignment: MainAxisAlignment.center, children: [ TButton( @@ -36,13 +33,10 @@ Widget _buildBlock(BuildContext context) { return TCalendar( title: '请选择日期', value: value, - height: size.height * 0.6 + 176, animateTo: true, - // 不使用popup时,useSafeArea无效 - useSafeArea: true, ); }, ), ], ); -} \ No newline at end of file +} diff --git a/tdesign-component/example/assets/code/calendar._buildCustomCell.txt b/tdesign-component/example/assets/code/calendar._buildCustomCell.txt deleted file mode 100644 index dc30963b4..000000000 --- a/tdesign-component/example/assets/code/calendar._buildCustomCell.txt +++ /dev/null @@ -1,120 +0,0 @@ - -Widget _buildCustomCell(BuildContext context) { - final size = MediaQuery.of(context).size; - final selected = ValueNotifier>( - [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]); - return ValueListenableBuilder( - valueListenable: selected, - builder: (context, value, child) { - final date = DateTime.fromMillisecondsSinceEpoch(value[0]); - return TCellGroup( - cells: [ - TCell( - title: '自定义日期单元格', - arrow: true, - note: '${date.year}-${date.month}-${date.day}', - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - onConfirm: (value) { - print('onConfirm:$value'); - selected.value = value; - }, - onClose: () { - print('onClose'); - }, - child: TCalendar( - title: '请选择日期', - value: value, - cellHeight: 80, - height: size.height * 0.6 + 176, - onCellClick: (value, type, tdate) { - print('onCellClick: $value'); - }, - onCellLongPress: (value, type, tdate) { - print('onCellLongPress: $value'); - }, - onHeaderClick: (index, week) { - print('onHeaderClick: $week'); - }, - onChange: (value) { - print('onChange: $value'); - }, - cellWidget: (context, tdate, selectType) { - final today = DateTime.now(); - //当前日期的自定义实现 - if (tdate.date.millisecondsSinceEpoch == - DateTime(today.year, today.month, today.day) - .millisecondsSinceEpoch && - selectType != DateSelectType.selected) { - return Container( - decoration: BoxDecoration( - color: TTheme.of(context).brandColor4, - borderRadius: BorderRadius.all(Radius.circular(6)), - ), - constraints: const BoxConstraints( - minWidth: 0, // 最小宽度为0 - maxWidth: double.infinity, // 最大宽度无限 - minHeight: 0, // 最小高度为0 - maxHeight: double.infinity), - child: const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('今天', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.white)), - ], - ), - ); - } - if (selectType == DateSelectType.selected) { - return Container( - decoration: BoxDecoration( - color: TTheme.of(context).successColor8, - borderRadius: BorderRadius.all(Radius.circular(6)), - ), - constraints: const BoxConstraints( - minWidth: 0, // 最小宽度为0 - maxWidth: double.infinity, // 最大宽度无限 - minHeight: 0, // 最小高度为0 - maxHeight: double.infinity), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('${tdate.date.day}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.white)), - const Text('文案文案', - style: TextStyle( - fontSize: 6, color: Colors.white)), - const Text('自定义', - style: TextStyle( - fontSize: 12, color: Colors.white)), - ], - ), - ); - } - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('${tdate.date.day}', - style: const TextStyle( - fontSize: 18, fontWeight: FontWeight.bold)), - const Text('文案文案', style: TextStyle(fontSize: 8)), - const Text('自定义', style: TextStyle(fontSize: 8)), - ], - ); - }), - ); - }, - ), - ], - ); - }, - ); -} \ No newline at end of file diff --git a/tdesign-component/example/assets/code/calendar._buildLunar.txt b/tdesign-component/example/assets/code/calendar._buildLunar.txt index 10c2065af..fc8128d23 100644 --- a/tdesign-component/example/assets/code/calendar._buildLunar.txt +++ b/tdesign-component/example/assets/code/calendar._buildLunar.txt @@ -85,8 +85,8 @@ Widget _buildLunar(BuildContext context) { height: 300, child: Column( children: [ - Padding( - padding: const EdgeInsets.all(16), + const Padding( + padding: EdgeInsets.all(16), child: Text( '选择年份', style: TextStyle( @@ -141,8 +141,8 @@ Widget _buildLunar(BuildContext context) { height: 400, child: Column( children: [ - Padding( - padding: const EdgeInsets.all(16), + const Padding( + padding: EdgeInsets.all(16), child: Text( '选择月份', style: TextStyle( diff --git a/tdesign-component/example/assets/code/calendar._buildStyle.txt b/tdesign-component/example/assets/code/calendar._buildStyle.txt index 6bbfac6c0..69d12f6e5 100644 --- a/tdesign-component/example/assets/code/calendar._buildStyle.txt +++ b/tdesign-component/example/assets/code/calendar._buildStyle.txt @@ -1,6 +1,5 @@ Widget _buildStyle(BuildContext context) { - final size = MediaQuery.of(context).size; const map = { 1: '初一', 2: '初二', @@ -8,93 +7,137 @@ Widget _buildStyle(BuildContext context) { 14: '情人节', 15: '元宵节', }; - return TCellGroup( - cells: [ - TCell( - title: '自定义文案', - arrow: true, - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - child: TCalendar( - 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: TTheme.of(context).fontTitleMedium?.size, - height: TTheme.of(context).fontTitleMedium?.height, - fontWeight: - TTheme.of(context).fontTitleMedium?.fontWeight, - color: TTheme.of(context).errorColor6, - ); - if (day?.typeNotifier.value == DateSelectType.selected) { - day?.style = day.style - ?.copyWith(color: TTheme.of(context).fontWhColor1); + + final customCellSelected = ValueNotifier>( + [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]); + + return ValueListenableBuilder( + valueListenable: customCellSelected, + builder: (context, cellValue, _) { + final cellDate = DateTime.fromMillisecondsSinceEpoch(cellValue[0]); + return TCellGroup( + cells: [ + // 1. 自定义文案(format 回调修改 prefix/suffix/style) + TCell( + title: '自定义文案', + arrow: true, + onClick: (cell) { + TCalendar.showPopup( + context, + title: '请选择日期', + 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: TTheme.of(context).fontTitleMedium?.size, + height: TTheme.of(context).fontTitleMedium?.height, + fontWeight: + TTheme.of(context).fontTitleMedium?.fontWeight, + color: TTheme.of(context).errorColor6, + ); + if (day?.typeNotifier.value == DateSelectType.selected) { + day?.style = day.style + ?.copyWith(color: TTheme.of(context).fontWhColor1); + } } } - } - return null; - }, - ), - ); - }, - ), - TCell( - title: '自定义按钮', - arrow: true, - onClick: (cell) { - late final TCalendarPopup calendar; - calendar = TCalendarPopup( - context, - visible: true, - confirmBtn: Padding( - padding: - EdgeInsets.symmetric(vertical: TTheme.of(context).spacer16), - child: TButton( - theme: TButtonTheme.danger, - shape: TButtonShape.round, - text: 'ok', - isBlock: true, - size: TButtonSize.large, - onTap: () { - print(calendar.selected); - calendar.close(); + return null; + }, + ); + }, + ), + + // 2. 自定义确认按钮 + TCell( + title: '自定义按钮', + arrow: true, + onClick: (cell) { + TCalendar.showPopup( + context, + title: '请选择日期', + value: [DateTime.now().millisecondsSinceEpoch], + confirmBtn: Padding( + padding: EdgeInsets.symmetric( + vertical: TTheme.of(context).spacer16), + child: const TButton( + theme: TButtonTheme.danger, + shape: TButtonShape.round, + text: 'ok', + isBlock: true, + size: TButtonSize.large, + ), + ), + onConfirm: (value) => print('confirmed: $value'), + ); + }, + ), + + // 3. 自定义日期单元格(cellWidget 回调) + TCell( + title: '自定义日期单元格', + arrow: true, + note: '${cellDate.year}-${cellDate.month}-${cellDate.day}', + onClick: (cell) { + TCalendar.showPopup( + context, + title: '请选择日期', + value: cellValue, + cellHeight: 80, + onConfirm: (value) => customCellSelected.value = value, + cellWidget: (context, tdate, selectType) { + final today = DateTime.now(); + final isToday = tdate.date.millisecondsSinceEpoch == + DateTime(today.year, today.month, today.day) + .millisecondsSinceEpoch; + + if (isToday && selectType != DateSelectType.selected) { + return _CustomCellContainer( + color: TTheme.of(context).brandColor4, + child: const Text('今天', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white)), + ); + } + if (selectType == DateSelectType.selected) { + return _CustomCellContainer( + color: TTheme.of(context).successColor8, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('${tdate.date.day}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white)), + const Text('已选', + style: + TextStyle(fontSize: 10, color: Colors.white)), + ], + ), + ); + } + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('${tdate.date.day}', + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)), + const Text('自定义', style: TextStyle(fontSize: 8)), + ], + ); }, - ), - ), - child: TCalendar( - title: '请选择日期', - value: [DateTime.now().millisecondsSinceEpoch], - height: size.height * 0.6 + 176, - ), - ); - }, - ), - TCell( - title: '自定义日期区间', - arrow: true, - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - child: TCalendar( - title: '请选择日期', - minDate: DateTime(2000, 1, 1).millisecondsSinceEpoch, - maxDate: DateTime(3000, 1, 1).millisecondsSinceEpoch, - value: [DateTime(2024, 10, 1).millisecondsSinceEpoch], - height: size.height * 0.6 + 176, - ), - ); - }, - ), - ], + ); + }, + ), + ], + ); + }, ); } \ No newline at end of file diff --git a/tdesign-component/example/lib/page/t_calendar_page.dart b/tdesign-component/example/lib/page/t_calendar_page.dart index 5c7841503..684871fba 100644 --- a/tdesign-component/example/lib/page/t_calendar_page.dart +++ b/tdesign-component/example/lib/page/t_calendar_page.dart @@ -4,6 +4,16 @@ import '../../base/example_widget.dart'; import '../annotation/demo.dart'; import '../lunar_data_source_example.dart'; +/// TCalendar 日历组件示例页 +/// +/// 演示 [TCalendar] 的所有使用方式: +/// - **Popup 模式**:通过 [TCalendar.showPopup] 以弹窗形式展示日历 +/// - 单选、多选、区间选择 +/// - 单选 + 时间、区间 + 时间 +/// - 锚点定位 +/// - **内嵌模式**:直接嵌入页面布局 +/// - **自定义样式**:文案、按钮、日期区间、日期单元格 +/// - **农历日历**:结合 [TCalendarDataSource] 展示农历信息 class TCalendarPage extends StatelessWidget { const TCalendarPage({super.key}); @@ -25,21 +35,13 @@ class TCalendarPage extends StatelessWidget { ]), ExampleModule(title: '组件样式', children: [ ExampleItem( - desc: '可以自由定义想要的风格', + desc: '自定义文案、按钮、单元格', ignoreCode: true, center: false, builder: (BuildContext context) { return const CodeWrapper(builder: _buildStyle); }, ), - ExampleItem( - desc: '自定义日期单元格', - ignoreCode: true, - center: false, - builder: (BuildContext context) { - return const CodeWrapper(builder: _buildCustomCell); - }, - ), ExampleItem( desc: '不使用Popup', ignoreCode: true, @@ -69,6 +71,14 @@ Widget _buildSimple(BuildContext context) { } /// 「组件类型」演示容器 +/// +/// 包含 6 种 [TCalendar.showPopup] 弹窗模式: +/// 1. 单选 + 天气 bottom +/// 2. 多选 + 已选汇总 bottom +/// 3. 区间选择 + 区间摘要 bottom +/// 4. 单选 + 时间选择器 bottom +/// 5. 区间 + 双时间选择器 bottom(Tab 切换开始/结束) +/// 6. 锚点定位到指定月份 class _SimpleDemo extends StatelessWidget { const _SimpleDemo(); @@ -88,6 +98,11 @@ class _SimpleDemo extends StatelessWidget { } // ========================= 1. 单选 + 天气 ========================= +/// 单选日历 + bottom 天气面板 +/// +/// 演示 [TCalendar.showPopup] 的 bottom / bottomExpanded 用法: +/// - 选中日期后展开 bottom 区域显示天气信息 +/// - 确认后回传选中值 class _SingleCalendarCell extends StatefulWidget { const _SingleCalendarCell(); @override @@ -112,31 +127,26 @@ class _SingleCalendarCellState extends State<_SingleCalendarCell> { @override Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; return TCell( title: '单个选择日历', arrow: true, note: _formatYmd(_selected), onClick: (_) { _expanded.value = _selected.isNotEmpty; - TCalendarPopup( + TCalendar.showPopup( context, - visible: true, + title: '请选择日期', + value: _selected, + bottomExpanded: _expanded, + onCellClick: (value, type, tdate) => _expanded.value = true, + bottom: (bCtx, dates) { + final d = dates.isEmpty + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(dates.first); + return _WeatherPanel(date: d, weather: _weatherFor(d)); + }, onConfirm: (value) => setState(() => _selected = value), onClose: () => _expanded.value = false, - builder: (ctx) => TCalendar( - title: '请选择日期', - value: _selected, - height: size.height * 0.6 + 176, - bottomExpanded: _expanded, - onCellClick: (value, type, tdate) => _expanded.value = true, - bottom: (bCtx, dates) { - final d = dates.isEmpty - ? DateTime.now() - : DateTime.fromMillisecondsSinceEpoch(dates.first); - return _WeatherPanel(date: d, weather: _weatherFor(d)); - }, - ), ); }, ); @@ -144,6 +154,9 @@ class _SingleCalendarCellState extends State<_SingleCalendarCell> { } // ========================= 2. 多选 ========================= +/// 多选日历 + bottom 已选汇总 +/// +/// 演示 [CalendarType.multiple] 多选模式,bottom 区域展示已选日期列表。 class _MultipleCalendarCell extends StatefulWidget { const _MultipleCalendarCell(); @override @@ -155,25 +168,20 @@ class _MultipleCalendarCellState extends State<_MultipleCalendarCell> { @override Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; return TCell( title: '多个选择日历', arrow: true, note: _dates.isEmpty ? '--' : '已选 ${_dates.length} 天', onClick: (_) { - TCalendarPopup( + TCalendar.showPopup( context, - visible: true, + title: '请选择日期', + type: CalendarType.multiple, + value: _dates.isEmpty + ? [DateTime.now().millisecondsSinceEpoch] + : _dates, + bottom: (bCtx, dates) => _MultipleSummary(selected: dates), onConfirm: (value) => setState(() => _dates = value), - builder: (ctx) => TCalendar( - title: '请选择日期', - type: CalendarType.multiple, - value: _dates.isEmpty - ? [DateTime.now().millisecondsSinceEpoch] - : _dates, - height: size.height * 0.6 + 176, - bottom: (bCtx, dates) => _MultipleSummary(selected: dates), - ), ); }, ); @@ -181,6 +189,9 @@ class _MultipleCalendarCellState extends State<_MultipleCalendarCell> { } // ========================= 3. 区间 ========================= +/// 区间选择日历 + bottom 区间摘要 +/// +/// 演示 [CalendarType.range] 区间模式,bottom 区域展示开始/结束日期及天数。 class _RangeCalendarCell extends StatefulWidget { const _RangeCalendarCell(); @override @@ -195,7 +206,6 @@ class _RangeCalendarCellState extends State<_RangeCalendarCell> { @override Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; return TCell( title: '区间选择日历', arrow: true, @@ -203,17 +213,13 @@ class _RangeCalendarCellState extends State<_RangeCalendarCell> { ? '${_formatMd(_dates.first)} ~ ${_formatMd(_dates[1])}' : '--', onClick: (_) { - TCalendarPopup( + TCalendar.showPopup( context, - visible: true, + title: '请选择日期区间', + type: CalendarType.range, + value: _dates, + bottom: (bCtx, dates) => _RangeSummary(selected: dates), onConfirm: (value) => setState(() => _dates = value), - builder: (ctx) => TCalendar( - title: '请选择日期区间', - type: CalendarType.range, - value: _dates, - height: size.height * 0.6 + 176, - bottom: (bCtx, dates) => _RangeSummary(selected: dates), - ), ); }, ); @@ -221,6 +227,10 @@ class _RangeCalendarCellState extends State<_RangeCalendarCell> { } // ========================= 4. 单选 + 时间 ========================= +/// 单选日历 + 时间选择器 +/// +/// 演示 bottom 区域放置 [TPicker] 时间选择器, +/// 确认时将日期和时间合并为完整时间戳。 class _SingleTimeCalendarCell extends StatefulWidget { const _SingleTimeCalendarCell(); @override @@ -251,7 +261,6 @@ class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> { @override Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; final d = DateTime.fromMillisecondsSinceEpoch(_selected.first); return TCell( title: '单个选择日历和时间', @@ -261,12 +270,22 @@ class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> { '${d.hour.toString().padLeft(2, '0')}:' '${d.minute.toString().padLeft(2, '0')}', onClick: (_) { - // 弹窗内的时分状态,生命周期与弹窗绑定 var popupHour = _hour; var popupMinute = _minute; - TCalendarPopup( + TCalendar.showPopup( context, - visible: true, + title: '请选择日期和时间', + fixedHeight: 780, + value: _selected, + bottom: (_, __) => _TimePickerPanel( + initialHour: popupHour, + initialMinute: popupMinute, + title: '选择时间', + onChange: (hour, min) { + popupHour = hour; + popupMinute = min; + }, + ), onConfirm: (dates) { final merged = dates.map((ms) { return DateTime.fromMillisecondsSinceEpoch(ms) @@ -284,20 +303,6 @@ class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> { _minute = popupMinute; }); }, - builder: (ctx) => TCalendar( - title: '请选择日期和时间', - value: _selected, - height: size.height * 0.92, - bottom: (_, __) => _TimePickerPanel( - initialHour: popupHour, - initialMinute: popupMinute, - title: '选择时间', - onChange: (h, m) { - popupHour = h; - popupMinute = m; - }, - ), - ), ); }, ); @@ -305,6 +310,11 @@ class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> { } // ========================= 5. 区间 + 时间 ========================= +/// 区间选择 + 双时间选择器 +/// +/// 演示 bottom 区域使用 [TTabBar] + [TPicker] 组合, +/// 点击日期单元格时自动切换开始/结束时间 Tab, +/// 确认时分别合并开始和结束的日期+时间。 class _RangeTimeCalendarCell extends StatefulWidget { const _RangeTimeCalendarCell(); @override @@ -335,7 +345,6 @@ class _RangeTimeCalendarCellState extends State<_RangeTimeCalendarCell> { @override Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; return TCell( title: '区间选择日历和时间', arrow: true, @@ -343,14 +352,34 @@ class _RangeTimeCalendarCellState extends State<_RangeTimeCalendarCell> { ? '${_formatMdHm(_dates.first)} ~ ${_formatMdHm(_dates.last)}' : '--', onClick: (_) { - // 弹窗内的时分状态,生命周期与弹窗绑定 var popupStartTime = List.from(_startTime); var popupEndTime = List.from(_endTime); - // GlobalKey 在 onClick 作用域内创建,与弹窗生命周期一致 final panelKey = GlobalKey<_RangeTimePickerPanelState>(); - TCalendarPopup( + TCalendar.showPopup( context, - visible: true, + title: '请选择日期和时间区间', + fixedHeight: 780, + type: CalendarType.range, + value: _dates, + onCellClick: (value, type, tdate) { + if (type == DateSelectType.start) { + panelKey.currentState?.switchTab(0); + } else if (type == DateSelectType.end) { + panelKey.currentState?.switchTab(1); + } + }, + bottom: (_, __) => _RangeTimePickerPanel( + key: panelKey, + initialStartTime: popupStartTime, + initialEndTime: popupEndTime, + onChanged: (isStart, hour, min) { + if (isStart) { + popupStartTime = [hour, min]; + } else { + popupEndTime = [hour, min]; + } + }, + ), onConfirm: (value) { if (value.length < 2) { return; @@ -379,34 +408,6 @@ class _RangeTimeCalendarCellState extends State<_RangeTimeCalendarCell> { _endTime = popupEndTime; }); }, - builder: (ctx) { - return TCalendar( - title: '请选择日期和时间区间', - height: size.height * 0.92, - type: CalendarType.range, - value: _dates, - // 点击开始日期 → 切到「开始时间」tab;点击结束日期 → 切到「结束时间」tab - onCellClick: (value, type, tdate) { - if (type == DateSelectType.start) { - panelKey.currentState?.switchTab(0); - } else if (type == DateSelectType.end) { - panelKey.currentState?.switchTab(1); - } - }, - bottom: (_, __) => _RangeTimePickerPanel( - key: panelKey, - initialStartTime: popupStartTime, - initialEndTime: popupEndTime, - onChanged: (isStart, h, m) { - if (isStart) { - popupStartTime = [h, m]; - } else { - popupEndTime = [h, m]; - } - }, - ), - ); - }, ); }, ); @@ -414,6 +415,10 @@ class _RangeTimeCalendarCellState extends State<_RangeTimeCalendarCell> { } // ========================= 6. 锚点 ========================= +/// 锚点定位 +/// +/// 演示 [TCalendar.showPopup] 的 anchorDate 参数, +/// 弹出日历时自动滚动到指定月份。 class _AnchorCalendarCell extends StatefulWidget { const _AnchorCalendarCell(); @override @@ -421,30 +426,21 @@ class _AnchorCalendarCell extends StatefulWidget { } class _AnchorCalendarCellState extends State<_AnchorCalendarCell> { - late List _selected = [ - DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000, - ]; + List _selected = [DateTime(2026, 5, 1).millisecondsSinceEpoch]; @override Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; return TCell( title: '添加锚点', arrow: true, note: _formatYmd(_selected), onClick: (_) { - TCalendarPopup( + TCalendar.showPopup( context, - visible: true, + title: '请选择日期', + anchorDate: DateTime(2026), + value: _selected, onConfirm: (dates) => setState(() => _selected = dates), - builder: (ctx) => TCalendar( - title: '请选择日期', - minDate: DateTime(2022, 1, 1).millisecondsSinceEpoch, - maxDate: DateTime(2028, 2, 15).millisecondsSinceEpoch, - anchorDate: DateTime(2026, 5), - value: _selected, - height: size.height * 0.6 + 176, - ), ); }, ); @@ -452,6 +448,7 @@ class _AnchorCalendarCellState extends State<_AnchorCalendarCell> { } // ===== 共用:构建时间选择器 items ===== +/// 构建时间选择器的列数据(24 小时 × 60 分钟) TPickerColumns _buildTimeItems() => TPickerColumns([ [ for (int i = 0; i < 24; i++) @@ -534,6 +531,27 @@ class _WeatherData { // ===== 拆分出的私有 widget ===== +/// 自定义单元格容器:统一圆角 + 填充色 + 撑满约束 +class _CustomCellContainer extends StatelessWidget { + const _CustomCellContainer({required this.color, required this.child}); + + final Color color; + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: color, + borderRadius: const BorderRadius.all(Radius.circular(6)), + ), + constraints: const BoxConstraints.expand(), + alignment: Alignment.center, + child: child, + ); + } +} + class _WeatherPanel extends StatelessWidget { const _WeatherPanel({required this.date, required this.weather}); @@ -774,6 +792,9 @@ class _TimePickerPanelState extends State<_TimePickerPanel> { } /// 区间+时间 demo 的双时间选择器面板(Tab 切换开始/结束) +/// +/// 外部可通过 `switchTab` 方法驱动 Tab 切换(如日历 onCellClick 触发)。 +/// 每个 Tab 内部的时间选择值会被缓存,切回时恢复。 class _RangeTimePickerPanel extends StatefulWidget { const _RangeTimePickerPanel({ super.key, @@ -870,9 +891,9 @@ class _RangeTimePickerPanelState extends State<_RangeTimePickerPanel> } } +/// 「组件样式 - 自定义文案 / 按钮 / 单元格」 @Demo(group: 'calendar') Widget _buildStyle(BuildContext context) { - final size = MediaQuery.of(context).size; const map = { 1: '初一', 2: '初二', @@ -880,109 +901,154 @@ Widget _buildStyle(BuildContext context) { 14: '情人节', 15: '元宵节', }; - return TCellGroup( - cells: [ - TCell( - title: '自定义文案', - arrow: true, - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - child: TCalendar( - 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: TTheme.of(context).fontTitleMedium?.size, - height: TTheme.of(context).fontTitleMedium?.height, - fontWeight: - TTheme.of(context).fontTitleMedium?.fontWeight, - color: TTheme.of(context).errorColor6, - ); - if (day?.typeNotifier.value == DateSelectType.selected) { - day?.style = day.style - ?.copyWith(color: TTheme.of(context).fontWhColor1); + + final customCellSelected = ValueNotifier>( + [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]); + + return ValueListenableBuilder( + valueListenable: customCellSelected, + builder: (context, cellValue, _) { + final cellDate = DateTime.fromMillisecondsSinceEpoch(cellValue[0]); + return TCellGroup( + cells: [ + // 1. 自定义文案(format 回调修改 prefix/suffix/style) + TCell( + title: '自定义文案', + arrow: true, + onClick: (cell) { + TCalendar.showPopup( + context, + title: '请选择日期', + 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: TTheme.of(context).fontTitleMedium?.size, + height: TTheme.of(context).fontTitleMedium?.height, + fontWeight: + TTheme.of(context).fontTitleMedium?.fontWeight, + color: TTheme.of(context).errorColor6, + ); + if (day?.typeNotifier.value == DateSelectType.selected) { + day?.style = day.style + ?.copyWith(color: TTheme.of(context).fontWhColor1); + } } } - } - return null; - }, - ), - ); - }, - ), - TCell( - title: '自定义按钮', - arrow: true, - onClick: (cell) { - late final TCalendarPopup calendar; - calendar = TCalendarPopup( - context, - visible: true, - confirmBtn: Padding( - padding: - EdgeInsets.symmetric(vertical: TTheme.of(context).spacer16), - child: TButton( - theme: TButtonTheme.danger, - shape: TButtonShape.round, - text: 'ok', - isBlock: true, - size: TButtonSize.large, - onTap: () { - print(calendar.selected); - calendar.close(); + return null; }, - ), - ), - child: TCalendar( - title: '请选择日期', - value: [DateTime.now().millisecondsSinceEpoch], - height: size.height * 0.6 + 176, - ), - ); - }, - ), - TCell( - title: '自定义日期区间', - arrow: true, - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - child: TCalendar( - title: '请选择日期', - minDate: DateTime(2000, 1, 1).millisecondsSinceEpoch, - maxDate: DateTime(3000, 1, 1).millisecondsSinceEpoch, - value: [DateTime(2024, 10, 1).millisecondsSinceEpoch], - height: size.height * 0.6 + 176, - ), - ); - }, - ), - ], + ); + }, + ), + + // 2. 自定义确认按钮 + TCell( + title: '自定义按钮', + arrow: true, + onClick: (cell) { + TCalendar.showPopup( + context, + title: '请选择日期', + value: [DateTime.now().millisecondsSinceEpoch], + confirmBtn: Padding( + padding: EdgeInsets.symmetric( + vertical: TTheme.of(context).spacer16), + child: const TButton( + theme: TButtonTheme.danger, + shape: TButtonShape.round, + text: 'ok', + isBlock: true, + size: TButtonSize.large, + ), + ), + onConfirm: (value) => print('confirmed: $value'), + ); + }, + ), + + // 3. 自定义日期单元格(cellWidget 回调) + TCell( + title: '自定义日期单元格', + arrow: true, + note: '${cellDate.year}-${cellDate.month}-${cellDate.day}', + onClick: (cell) { + TCalendar.showPopup( + context, + title: '请选择日期', + value: cellValue, + cellHeight: 80, + onConfirm: (value) => customCellSelected.value = value, + cellWidget: (context, tdate, selectType) { + final today = DateTime.now(); + final isToday = tdate.date.millisecondsSinceEpoch == + DateTime(today.year, today.month, today.day) + .millisecondsSinceEpoch; + + if (isToday && selectType != DateSelectType.selected) { + return _CustomCellContainer( + color: TTheme.of(context).brandColor4, + child: const Text('今天', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white)), + ); + } + if (selectType == DateSelectType.selected) { + return _CustomCellContainer( + color: TTheme.of(context).successColor8, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('${tdate.date.day}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white)), + const Text('已选', + style: + TextStyle(fontSize: 10, color: Colors.white)), + ], + ), + ); + } + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('${tdate.date.day}', + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)), + const Text('自定义', style: TextStyle(fontSize: 8)), + ], + ); + }, + ); + }, + ), + ], + ); + }, ); } +/// 「组件样式 - 不使用 Popup(内嵌模式)」 +/// +/// 直接将 [TCalendar] 嵌入页面布局,配合 [ValueListenableBuilder] +/// 实现外部按钮控制选中日期的增减。 @Demo(group: 'calendar') Widget _buildBlock(BuildContext context) { - final size = MediaQuery.of(context).size; final selected = ValueNotifier>( [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000], ); return Column( - // spacing: TTheme.of(context).spacer16, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - // spacing: TTheme.of(context).spacer16, mainAxisAlignment: MainAxisAlignment.center, children: [ TButton( @@ -1009,10 +1075,7 @@ Widget _buildBlock(BuildContext context) { return TCalendar( title: '请选择日期', value: value, - height: size.height * 0.6 + 176, animateTo: true, - // 不使用popup时,useSafeArea无效 - useSafeArea: true, ); }, ), @@ -1020,130 +1083,13 @@ Widget _buildBlock(BuildContext context) { ); } -@Demo(group: 'calendar') -Widget _buildCustomCell(BuildContext context) { - final size = MediaQuery.of(context).size; - final selected = ValueNotifier>( - [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]); - return ValueListenableBuilder( - valueListenable: selected, - builder: (context, value, child) { - final date = DateTime.fromMillisecondsSinceEpoch(value[0]); - return TCellGroup( - cells: [ - TCell( - title: '自定义日期单元格', - arrow: true, - note: '${date.year}-${date.month}-${date.day}', - onClick: (cell) { - TCalendarPopup( - context, - visible: true, - onConfirm: (value) { - print('onConfirm:$value'); - selected.value = value; - }, - onClose: () { - print('onClose'); - }, - child: TCalendar( - title: '请选择日期', - value: value, - cellHeight: 80, - height: size.height * 0.6 + 176, - onCellClick: (value, type, tdate) { - print('onCellClick: $value'); - }, - onCellLongPress: (value, type, tdate) { - print('onCellLongPress: $value'); - }, - onHeaderClick: (index, week) { - print('onHeaderClick: $week'); - }, - onChange: (value) { - print('onChange: $value'); - }, - cellWidget: (context, tdate, selectType) { - final today = DateTime.now(); - //当前日期的自定义实现 - if (tdate.date.millisecondsSinceEpoch == - DateTime(today.year, today.month, today.day) - .millisecondsSinceEpoch && - selectType != DateSelectType.selected) { - return Container( - decoration: BoxDecoration( - color: TTheme.of(context).brandColor4, - borderRadius: BorderRadius.all(Radius.circular(6)), - ), - constraints: const BoxConstraints( - minWidth: 0, // 最小宽度为0 - maxWidth: double.infinity, // 最大宽度无限 - minHeight: 0, // 最小高度为0 - maxHeight: double.infinity), - child: const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('今天', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.white)), - ], - ), - ); - } - if (selectType == DateSelectType.selected) { - return Container( - decoration: BoxDecoration( - color: TTheme.of(context).successColor8, - borderRadius: BorderRadius.all(Radius.circular(6)), - ), - constraints: const BoxConstraints( - minWidth: 0, // 最小宽度为0 - maxWidth: double.infinity, // 最大宽度无限 - minHeight: 0, // 最小高度为0 - maxHeight: double.infinity), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('${tdate.date.day}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.white)), - const Text('文案文案', - style: TextStyle( - fontSize: 6, color: Colors.white)), - const Text('自定义', - style: TextStyle( - fontSize: 12, color: Colors.white)), - ], - ), - ); - } - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('${tdate.date.day}', - style: const TextStyle( - fontSize: 18, fontWeight: FontWeight.bold)), - const Text('文案文案', style: TextStyle(fontSize: 8)), - const Text('自定义', style: TextStyle(fontSize: 8)), - ], - ); - }), - ); - }, - ), - ], - ); - }, - ); -} - +/// 「组件样式 - 农历日历」 +/// +/// 演示内嵌模式下结合 [TCalendarDataSource] 展示农历信息, +/// 支持月份切换、年份/月份跳转、农历信息开关。 +/// 点击日期时通过 SnackBar 显示完整的农历/节气/节日/假期信息。 @Demo(group: 'calendar') Widget _buildLunar(BuildContext context) { - final size = MediaQuery.of(context).size; final dataSource = LunarDataSourceExample(); // 当前月份状态 @@ -1228,8 +1174,8 @@ Widget _buildLunar(BuildContext context) { height: 300, child: Column( children: [ - Padding( - padding: const EdgeInsets.all(16), + const Padding( + padding: EdgeInsets.all(16), child: Text( '选择年份', style: TextStyle( @@ -1284,8 +1230,8 @@ Widget _buildLunar(BuildContext context) { height: 400, child: Column( children: [ - Padding( - padding: const EdgeInsets.all(16), + const Padding( + padding: EdgeInsets.all(16), child: Text( '选择月份', style: TextStyle( @@ -1397,7 +1343,6 @@ Widget _buildLunar(BuildContext context) { showLunarInfo: show, dataSource: dataSource, value: value, - height: size.height * 0.6, onChange: (newValue) { selectedDate.value = newValue; diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart index 586fbf024..c42ee56f2 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart @@ -8,10 +8,67 @@ export 't_calendar_body.dart'; export 't_calendar_cell.dart'; export 't_calendar_data_source.dart'; export 't_calendar_header.dart'; -export 't_calendar_popup.dart'; export 't_calendar_style.dart'; export 't_lunar_date.dart'; +// --------------------------------------------------------------------------- +// TCalendarInherited — 日历弹窗状态托管,供 TCalendar 内部读取 +// --------------------------------------------------------------------------- + +/// 日历弹窗状态的 InheritedWidget 容器。 +/// +/// 由上层(如 [TSlidePopupRoute] 的 builder)包裹在 [TCalendar] 外侧, +/// 将选中态、确认/关闭回调等注入子树。 +class TCalendarInherited extends InheritedWidget { + const TCalendarInherited({ + required Widget child, + this.onClose, + required this.selected, + this.usePopup = true, + this.popupControls = true, + this.popupConfirmBtn, + this.onConfirm, + this.confirmBtn, + Key? key, + }) : super(child: child, key: key); + + final VoidCallback? onClose; + + /// 选中态的可写引用(仅供 [TCalendar] 内部更新使用)。 + /// + /// 对外消费方请使用 [selectedListenable] 这一只读视图。 + final ValueNotifier> selected; + + /// 选中态的只读视图,供下游 widget 监听变化。 + ValueListenable> get selectedListenable => selected; + + final bool? usePopup; + + /// 是否由 [TCalendar] 自行渲染关闭按钮和标题行。 + /// + /// 为 `true`(默认)时 [TCalendar] 渲染关闭按钮与标题行; + /// 为 `false` 时由外层面板(如 [TPopupBottomDisplayPanel])承载。 + final bool popupControls; + + /// 是否由 [TCalendar] 渲染底部确认按钮。 + /// + /// 为 `null`(默认)时跟随 [popupControls];显式设置时覆盖。 + final bool? popupConfirmBtn; + + /// 实际是否渲染底部确认按钮。 + bool get effectivePopupConfirmBtn => popupConfirmBtn ?? popupControls; + + final VoidCallback? onConfirm; + final Widget? confirmBtn; + + @override + bool updateShouldNotify(covariant TCalendarInherited oldWidget) => false; + + static TCalendarInherited? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } +} + typedef CalendarFormat = TDate? Function(TDate? day); /// [TCalendar.bottom] 的构建器签名。`selectedDates` 为只读视图,详细行为见 [TCalendar.bottom]。 @@ -69,10 +126,10 @@ class TCalendar extends StatefulWidget { /// 用于格式化日期的函数,可定义日期前后的显示内容和日期样式 final CalendarFormat? format; - /// 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认半年后 + /// 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认 2100-12-31 final int? maxDate; - /// 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认今天 + /// 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认 1970-01-01 final int? minDate; /// 标题 @@ -90,7 +147,7 @@ class TCalendar extends StatefulWidget { /// 年月显示格式,`year`表示年,`month`表示月,如`year month`表示年在前、月在后、中间隔一个空格 final String? displayFormat; - /// 高度 + /// 高度,不传时内嵌模式自动按 5 行日期计算 final double? height; /// 日期高度 @@ -136,32 +193,31 @@ class TCalendar extends StatefulWidget { /// 底部自定义区域构建器,以浮层方式叠加在日历主体之上。 /// - /// **仅能在 [TCalendarPopup] 内使用**(作为其 [child] / [builder] 的子树)。 + /// **仅能在 `TPopupBottomDisplayPanel` 内使用**(作为其 `child` 的子树)。 /// - /// - **不会撑高 [TCalendar]**,请在 [height] 中预留 bottom 自身的占用高度; + /// - **不会撑高 [TCalendar]**,请在面板 `fixedHeight` 中预留 bottom 自身的占用高度; /// - `selectedDates` 随弹窗内日期点击实时更新; /// - 传入的 `selectedDates` 是只读视图([List.unmodifiable]),请勿原地修改。 /// /// ```dart - /// TCalendarPopup( - /// context, + /// TPopupBottomDisplayPanel( + /// fixedHeight: 600, /// child: TCalendar( - /// height: 600, /// bottom: (ctx, dates) => MyFooter(selectedDates: dates), /// ), /// ); /// ``` final CalendarBottomBuilder? bottom; - /// bottom 区域是否展开(响应式)。**仅能在 [TCalendarPopup] 内使用。** + /// bottom 区域是否展开(响应式)。**仅能在 [TPopupBottomDisplayPanel] 内使用。** /// /// 为 `null`(默认)时 bottom 始终展开;传入 [ValueListenable] 时, /// bottom 展开/收起将跟随其值变化播放滑动动画,常配合 [ValueNotifier] 使用。 /// /// ```dart /// final expanded = ValueNotifier(false); - /// TCalendarPopup( - /// context, + /// TPopupBottomDisplayPanel( + /// fixedHeight: 600, /// child: TCalendar( /// bottomExpanded: expanded, /// onCellClick: (v, t, d) => expanded.value = true, @@ -204,6 +260,217 @@ class TCalendar extends StatefulWidget { return DateTime(date.year, date.month, date.day); }).toList(); + // --------------------------------------------------------------------------- + // 默认高度计算常量 + // --------------------------------------------------------------------------- + static const double _kPanelHeaderHeight = 58.0; + static const double _kWeekdayHeight = 46.0; + static const double _kMonthTitleHeight = 22.0; + static const double _kCellHeight = 60.0; + static const double _kVerticalGap = 8.0; + static const int _kVisibleRows = 5; + static const double _kConfirmBtnAreaHeight = 64.0; + static const double _kBodyPadding = 16.0; + static const double _kPopupHeightRatio = 0.9; + + static double _calcDefaultHeight(double safeBottom, double screenHeight) { + const calendarContentHeight = _kWeekdayHeight + + _kMonthTitleHeight + + _kVisibleRows * (_kVerticalGap + _kCellHeight) + + _kBodyPadding * 2; + final idealHeight = _kPanelHeaderHeight + + calendarContentHeight + + _kConfirmBtnAreaHeight + + safeBottom; + return idealHeight.clamp(0.0, screenHeight * _kPopupHeightRatio); + } + + /// 弹出日历选择器,返回选中的日期列表。 + /// + /// 取消或关闭弹窗时返回 `null`;点击确认时返回选中日期的毫秒时间戳列表。 + /// + /// ```dart + /// final result = await TCalendar.showPopup( + /// context, + /// title: '请选择日期', + /// type: CalendarType.single, + /// ); + /// if (result != null) { + /// print('选中了: $result'); + /// } + /// ``` + /// + /// 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + /// + [TSlidePopupRoute] 自行组装。 + static Future?> showPopup( + BuildContext context, { + /// 弹窗标题 + String? title, + + /// 日历选择类型 + CalendarType type = CalendarType.single, + + /// 当前选中的日期(毫秒时间戳列表) + List? value, + + /// 最小可选日期(毫秒时间戳) + int? minDate, + + /// 最大可选日期(毫秒时间戳) + int? maxDate, + + /// 锚点日期,弹出时滚动到该月 + DateTime? anchorDate, + + /// 面板固定高度(不传时自动计算) + double? fixedHeight, + + /// 第一天从星期几开始,默认 0 = 周日 + int? firstDayOfWeek, + + /// 年月显示格式 + String? displayFormat, + + /// 日期高度 + double? cellHeight, + + /// 自定义样式 + TCalendarStyle? style, + + /// 用于格式化日期的函数 + CalendarFormat? format, + + /// 底部自定义区域构建器 + CalendarBottomBuilder? bottom, + + /// bottom 区域是否展开(响应式) + ValueListenable? bottomExpanded, + + /// 自定义确认按钮 + Widget? confirmBtn, + + /// 点击确认的额外回调(除了返回值之外) + void Function(List)? onConfirm, + + /// 弹窗关闭后的回调 + VoidCallback? onClose, + + /// 点击日期时触发 + void Function(int value, DateSelectType type, TDate tdate)? onCellClick, + + /// 长按日期时触发 + void Function(int value, DateSelectType type, TDate tdate)? onCellLongPress, + + /// 点击遮罩或物理返回是否关闭 + bool autoClose = true, + + /// 面板是否可拖动 + bool draggable = false, + + /// 自定义日期单元格组件 + Widget? Function( + BuildContext context, + TDate tdate, + DateSelectType selectType, + )? cellWidget, + + /// 日历类型:阳历或农历 + TCalendarDateType dateType = TCalendarDateType.solar, + + /// 外部数据源,用于提供农历转换等功能 + TCalendarDataSource? dataSource, + + /// 阳历模式下是否显示农历信息作为副标题 + bool showLunarInfo = false, + + /// 月份变化时触发 + ValueChanged? onMonthChange, + + /// 月标题构建器 + Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, + }) async { + final selected = ValueNotifier>(value ?? []); + List? result; + var closing = false; + + void doClose(NavigatorState nav) { + if (closing) { + return; + } + closing = true; + nav.pop(); + } + + await Navigator.of(context).push(TSlidePopupRoute( + isDismissible: autoClose, + slideTransitionFrom: SlideTransitionFrom.bottom, + builder: (ctx) { + final nav = Navigator.of(context); + final safeBottom = MediaQuery.of(ctx).padding.bottom; + final screenHeight = MediaQuery.of(ctx).size.height; + + final panelHeight = + fixedHeight ?? _calcDefaultHeight(safeBottom, screenHeight); + + return TCalendarInherited( + selected: selected, + usePopup: true, + popupControls: false, + popupConfirmBtn: true, + confirmBtn: confirmBtn, + onClose: () { + if (autoClose) { + doClose(nav); + } + }, + onConfirm: () { + result = List.from(selected.value); + onConfirm?.call(result!); + if (autoClose) { + doClose(nav); + } + }, + child: TPopupBottomDisplayPanel( + title: title, + draggable: draggable, + fixedHeight: panelHeight, + closeClick: () { + if (autoClose) { + doClose(nav); + } + }, + child: TCalendar( + title: title, + type: type, + value: value, + minDate: minDate, + maxDate: maxDate, + anchorDate: anchorDate, + firstDayOfWeek: firstDayOfWeek, + displayFormat: displayFormat ?? 'year month', + cellHeight: cellHeight, + style: style, + format: format, + bottom: bottom, + bottomExpanded: bottomExpanded, + onCellClick: onCellClick, + onCellLongPress: onCellLongPress, + cellWidget: cellWidget, + dateType: dateType, + dataSource: dataSource, + showLunarInfo: showLunarInfo, + onMonthChange: onMonthChange, + monthTitleBuilder: monthTitleBuilder, + ), + ), + ); + }, + )); + + onClose?.call(); + return result; + } + @override _TCalendarState createState() => _TCalendarState(); } @@ -278,11 +545,11 @@ class _TCalendarState extends State { void _assertPopupOnlyBottom() { assert( widget.bottom == null || _usePopupBottom, - '[TCalendar] bottom 仅能在 TCalendarPopup 内使用', + '[TCalendar] bottom 仅能在 TPopupBottomDisplayPanel 内使用', ); assert( widget.bottomExpanded == null || _usePopupBottom, - '[TCalendar] bottomExpanded 仅能在 TCalendarPopup 内使用', + '[TCalendar] bottomExpanded 仅能在 TPopupBottomDisplayPanel 内使用', ); } @@ -315,6 +582,7 @@ class _TCalendarState extends State { Widget stackContent(bool bottomExpanded) { return Stack( + fit: StackFit.expand, children: [ _buildMainColumn(verticalGap, hasBottom, bottomExpanded), if (hasBottom) _buildBottom(bottomExpanded), @@ -330,13 +598,23 @@ class _TCalendarState extends State { : stackContent(hasBottom); return Container( - height: widget.height, + height: widget.height ?? _calcInlineDefaultHeight(verticalGap), width: widget.width ?? double.infinity, decoration: _style.decoration, child: child, ); } + /// 当 [TCalendarInherited.popupControls] 为 `true` 时,由 [TCalendar] + /// 自行渲染关闭按钮与标题行;为 `false` 时由外层面板承载。 + bool get _showPopupControls => + (inherited?.usePopup ?? false) && (inherited?.popupControls ?? true); + + /// 是否渲染底部确认按钮,由 [TCalendarInherited.popupConfirmBtn] 控制。 + bool get _showPopupConfirmBtn => + (inherited?.usePopup ?? false) && + (inherited?.effectivePopupConfirmBtn ?? false); + Widget _buildMainColumn( double verticalGap, bool hasBottom, @@ -350,12 +628,12 @@ class _TCalendarState extends State { padding: TTheme.of(context).spacer16, weekdayStyle: _style.weekdayStyle, weekdayHeight: 46, - title: widget.title, + title: _showPopupControls ? widget.title : null, titleStyle: _style.titleStyle, - titleWidget: widget.titleWidget, + titleWidget: _showPopupControls ? widget.titleWidget : null, titleMaxLine: _style.titleMaxLine, titleOverflow: TextOverflow.ellipsis, - closeBtn: inherited?.usePopup ?? false, + closeBtn: _showPopupControls, closeColor: _style.titleCloseColor, weekdayNames: weekdayNames, onClose: inherited?.onClose, @@ -364,7 +642,7 @@ class _TCalendarState extends State { Expanded( child: _buildBodyArea(verticalGap, hasBottom, bottomExpanded), ), - if (inherited?.usePopup == true) + if (_showPopupConfirmBtn) inherited?.confirmBtn ?? Padding( padding: widget.useSafeArea == true @@ -506,7 +784,7 @@ class _TCalendarState extends State { ? MediaQuery.of(context).padding.bottom : 0.0; - if (inherited?.usePopup == true) { + if (_showPopupConfirmBtn) { final btnPadding = widget.useSafeArea == true ? TTheme.of(context).spacer16 : TTheme.of(context).spacer16 * 2; @@ -532,4 +810,19 @@ class _TCalendarState extends State { } return widget.showLunarInfo ? 80 : 60; } + + /// 内嵌模式下不传 `height` 时的默认高度。 + /// + /// 布局 = weekday(46) + monthTitle(22) + 5行(cellHeight + verticalGap) + bodyPadding*2 + double _calcInlineDefaultHeight(double verticalGap) { + const weekdayHeight = 46.0; + final monthTitleHeight = widget.monthTitleHeight ?? 22.0; + final cellHeight = _getEffectiveCellHeight(); + final bodyPadding = _style.bodyPadding ?? TTheme.of(context).spacer16; + const visibleRows = 5; + return weekdayHeight + + monthTitleHeight + + visibleRows * (cellHeight + verticalGap) + + bodyPadding * 2; + } } diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart index bcbd4697b..38b915d4b 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart @@ -1,12 +1,9 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import '../../../tdesign_flutter.dart'; import '../../util/context_extension.dart'; import '../../util/iterable_ext.dart'; -class TCalendarBody extends StatelessWidget { +class TCalendarBody extends StatefulWidget { const TCalendarBody({ Key? key, this.maxDate, @@ -60,86 +57,145 @@ class TCalendarBody extends StatelessWidget { final TCalendarDataSource? dataSource; @override - Widget build(BuildContext context) { - final scrollController = TrackingScrollController(); - final min = _getDefDate(minDate); - final max = _getDefDate(maxDate, 6); - final months = _monthsBetween(min, max); - final data = >{}; - final monthHeight = {}; - DateTime? _lastPrintMonth; - scrollController.addListener(() { - // 根据滚动位置判断当前是几月 - var currentOffset = 0.0; - for (var i = 0; i < months.length; i++) { - final mh = _getMonthHeight(months, i, monthHeight); - if (scrollController.offset >= currentOffset && - scrollController.offset < currentOffset + mh) { - //只返回下一个月 - DateTime currentMonth = months[i + 1]; - // 缓存上一次打印的月份,只有变更时才打印 + State createState() => _TCalendarBodyState(); +} + +class _TCalendarBodyState extends State { + late final ScrollController _scrollController; + DateTime? _lastPrintMonth; + final _data = >{}; + final _monthHeight = {}; + late List _months; + late DateTime _min; + late DateTime _max; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); + _initMonths(); + _scrollToItem(); + } + + @override + void didUpdateWidget(covariant TCalendarBody oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.minDate != widget.minDate || + oldWidget.maxDate != widget.maxDate) { + _monthHeight.clear(); + _data.clear(); + _lastPrintMonth = null; + _initMonths(); + } + if (oldWidget.anchorDate != widget.anchorDate || + oldWidget.value != widget.value) { + _scrollToItem(); + } + } + + @override + void dispose() { + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + } + + void _initMonths() { + _min = _getDefDate(widget.minDate); + _max = _getDefDate(widget.maxDate, true); + _months = _monthsBetween(_min, _max); + } + + int? _lastCleanupIndex; + + void _onScroll() { + var currentOffset = 0.0; + for (var i = 0; i < _months.length; i++) { + final mh = _getMonthHeight(_months, i, _monthHeight); + if (_scrollController.offset >= currentOffset && + _scrollController.offset < currentOffset + mh) { + if (i + 1 < _months.length) { + final currentMonth = _months[i + 1]; if (_lastPrintMonth == null || !_lastPrintMonth!.isAtSameMomentAs(currentMonth)) { _lastPrintMonth = currentMonth; - onMonthChange?.call(currentMonth); + widget.onMonthChange?.call(currentMonth); } - break; } - currentOffset += mh; + _cleanupCache(i); + break; } - }); - _scrollToItem(scrollController, months, monthHeight); + currentOffset += mh; + } + } + + /// 清理距离当前可见月份过远的缓存数据,避免在 itemBuilder 中执行副作用。 + void _cleanupCache(int currentIndex) { + if (_lastCleanupIndex == currentIndex) { + return; + } + _lastCleanupIndex = currentIndex; + final keysToRemove = []; + final keyList = [..._data.keys]; + for (var i = 0; i < keyList.length; i++) { + final monthIdx = _months.indexOf(keyList[i]); + if (monthIdx < currentIndex - 10 || monthIdx > currentIndex + 10) { + keysToRemove.add(keyList[i]); + } + } + for (final key in keysToRemove) { + _data.remove(key); + } + } + + @override + Widget build(BuildContext context) { return ListView.builder( - padding: EdgeInsets.all(bodyPadding), - controller: scrollController, - itemCount: months.length, + padding: EdgeInsets.all(widget.bodyPadding), + controller: _scrollController, + itemCount: _months.length, itemExtentBuilder: (index, dimensions) => - _getMonthHeight(months, index, monthHeight), + _getMonthHeight(_months, index, _monthHeight), itemBuilder: (context, index) { - final monthDate = months[index]; + final monthDate = _months[index]; final monthYear = monthDate.year.toString() + context.resource.year; - final monthMonth = monthNames[monthDate.month - 1]; - final monthDateText = displayFormat + final monthMonth = widget.monthNames[monthDate.month - 1]; + final monthDateText = widget.displayFormat .replaceFirst('year', monthYear) .replaceFirst('month', monthMonth); late List monthData; - if (data.containsKey(monthDate)) { - monthData = data[monthDate]!; + if (_data.containsKey(monthDate)) { + monthData = _data[monthDate]!; } else { - monthData = data[monthDate] = _getDaysInMonth(monthDate, min, max); + monthData = _data[monthDate] = + _getDaysInMonth(monthDate, _min, _max); } - final keyList = [...data.keys]; - final currentIndex = keyList.indexOf(monthDate); - keyList.forEachWidthIndex((key, index) { - if (index < currentIndex - 10 || index > currentIndex + 10) { - // 保留最近 10 个月的数据,防止内存泄露 - data.remove(key); - } - }); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - height: monthTitleHeight, - child: monthTitleBuilder?.call(context, monthDate) ?? - TText(monthDateText, style: monthTitleStyle), + height: widget.monthTitleHeight, + child: widget.monthTitleBuilder?.call(context, monthDate) ?? + TText(monthDateText, style: widget.monthTitleStyle), ), ...List.generate( (monthData.length / 7).ceil(), (rowIndex) => [ - SizedBox(height: verticalGap), + SizedBox(height: widget.verticalGap), Row( children: [ for (int colIndex = 0; colIndex < 7; colIndex++) ...[ - if (colIndex != 0) SizedBox(width: verticalGap / 2), + if (colIndex != 0) + SizedBox(width: widget.verticalGap / 2), Expanded( - child: builder( + child: widget.builder( (rowIndex * 7 + colIndex < monthData.length) ? monthData[rowIndex * 7 + colIndex] : null, monthData, - data, + _data, rowIndex, colIndex, ), @@ -149,60 +205,62 @@ class TCalendarBody extends StatelessWidget { ), ], ).expand((element) => element).toList(), - SizedBox(height: index == months.length - 1 ? 0 : bodyPadding), + SizedBox( + height: index == _months.length - 1 ? 0 : widget.bodyPadding), ], ); }, ); } - void _scrollToItem(ScrollController scrollController, List months, - Map monthHeight) { - DateTime? scrollToDate = anchorDate; + void _scrollToItem() { + var scrollToDate = widget.anchorDate; if (scrollToDate == null) { - if (value == null || value!.isEmpty) { + if (widget.value == null || widget.value!.isEmpty) { return; } - scrollToDate = value!.reduce((a, b) => a.isBefore(b) ? a : b); + scrollToDate = widget.value!.reduce((a, b) => a.isBefore(b) ? a : b); } - var lastMonthDay = DateTime(months.last.year, months.last.month + 1); + var lastMonthDay = DateTime(_months.last.year, _months.last.month + 1); lastMonthDay = lastMonthDay.add(const Duration(days: -1)); - if (months.first.isAfter(scrollToDate) || lastMonthDay.isBefore(scrollToDate)) { + if (_months.first.isAfter(scrollToDate) || + lastMonthDay.isBefore(scrollToDate)) { return; } + final targetDate = scrollToDate; WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } var height = 0.0; - for (var i = 0; i < months.length; i++) { - final item = months[i]; - if (item.year == scrollToDate!.year && item.month == scrollToDate!.month) { + for (var i = 0; i < _months.length; i++) { + final item = _months[i]; + if (item.year == targetDate.year && item.month == targetDate.month) { break; } - height += (_getMonthHeight(months, i, monthHeight) ?? 0); + height += _getMonthHeight(_months, i, _monthHeight); } if (height <= 0) { return; } - if (animateTo) { - scrollController.animateTo( + if (widget.animateTo) { + _scrollController.animateTo( height, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, ); } else { - scrollController.jumpTo(height); + _scrollController.jumpTo(height); } }); } - 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); + DateTime _getDefDate(int? date, [bool isMax = false]) { + if (date != null) { + final d = DateTime.fromMillisecondsSinceEpoch(date); + return DateTime(d.year, d.month, d.day); } - final month = now.month + addMonth; - return DateTime(now.year, date == null ? month : now.month, now.day); + return isMax ? DateTime(2100, 12, 31) : DateTime(1970); } List _monthsBetween(DateTime min, DateTime max) { @@ -225,24 +283,29 @@ class TCalendarBody extends StatelessWidget { 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) { + } else if (widget.type == CalendarType.single && + (widget.value?.length ?? 0) >= 1) { + if (date.compareTo(widget.value![0]) == 0) { selectType = DateSelectType.selected; } - } else if (type == CalendarType.multiple && value != null) { - if (value!.isContains((e) => date.compareTo(e) == 0)) { + } else if (widget.type == CalendarType.multiple && + widget.value != null) { + if (widget.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) { + } else if (widget.type == CalendarType.range && + (widget.value?.length ?? 0) >= 1) { + final end = + (widget.value?.length ?? 0) > 1 ? widget.value![1] : null; + if (date.compareTo(widget.value![0]) == 0) { selectType = DateSelectType.start; } - if (end != null && value![0].compareTo(end) < 0) { + if (end != null && widget.value![0].compareTo(end) < 0) { if (date.compareTo(end) == 0) { selectType = DateSelectType.end; } - if (date.compareTo(value![0]) == 1 && date.compareTo(end) == -1) { + if (date.compareTo(widget.value![0]) == 1 && + date.compareTo(end) == -1) { selectType = DateSelectType.centre; } } @@ -252,11 +315,11 @@ class TCalendarBody extends StatelessWidget { String? solarTerm; String? festival; Map? holidayInfo; - if (dataSource != null) { - lunarInfo = dataSource!.getLunarInfo(date); - solarTerm = dataSource!.getSolarTerm(date); - festival = dataSource!.getFestival(date, lunarInfo); - holidayInfo = dataSource!.getHolidayInfo(date); + if (widget.dataSource != null) { + lunarInfo = widget.dataSource!.getLunarInfo(date); + solarTerm = widget.dataSource!.getSolarTerm(date); + festival = widget.dataSource!.getFestival(date, lunarInfo); + holidayInfo = widget.dataSource!.getHolidayInfo(date); } daysInMonth.add(TDate( date: date, @@ -270,7 +333,9 @@ class TCalendarBody extends StatelessWidget { } var sufOffset = 7 - daysInMonth.length % 7; sufOffset = sufOffset == 7 ? 0 : sufOffset; - List.generate(sufOffset, (index) => daysInMonth.add(null)); + for (var i = 0; i < sufOffset; i++) { + daysInMonth.add(null); + } return daysInMonth; } @@ -279,7 +344,7 @@ class TCalendarBody extends StatelessWidget { final month = date.month; var dayOneWeek = DateTime(year, month).weekday; dayOneWeek = dayOneWeek == 7 ? 0 : dayOneWeek; - var preOffset = dayOneWeek - firstDayOfWeek; + var preOffset = dayOneWeek - widget.firstDayOfWeek; preOffset = preOffset < 0 ? preOffset + 7 : preOffset; return preOffset; } @@ -298,9 +363,9 @@ class TCalendarBody extends StatelessWidget { final preOffset = _getPreOffset(item); final daysInMonthCount = DateTime(item.year, item.month + 1, 0).day; final daysInMonth = preOffset + daysInMonthCount; - final height = monthTitleHeight + - (daysInMonth / 7).ceil() * (verticalGap + cellHeight) + - (isLast ? 0 : bodyPadding); + final height = widget.monthTitleHeight + + (daysInMonth / 7).ceil() * (widget.verticalGap + widget.cellHeight) + + (isLast ? 0 : widget.bodyPadding); monthHeight[index] = height; return height; } diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart b/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart index 1a4efd91e..5ab160935 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart @@ -234,7 +234,7 @@ class _TCalendarCellState extends State { Widget _buildDefaultCell( BuildContext context, TDate tdate, TCalendarStyle cellStyle) { // 根据 dateType 和 showLunarInfo 决定显示内容 - String mainText = widget.tdate!.date.day.toString(); + var mainText = widget.tdate!.date.day.toString(); String? subText; if (widget.dateType == TCalendarDateType.lunar && tdate.lunarInfo != null) { @@ -245,16 +245,13 @@ class _TCalendarCellState extends State { widget.showLunarInfo) { // 阳历模式+显示农历信息 mainText = widget.tdate!.date.day.toString(); - + // 优先级:节日 > 节气 > 农历日期 if (tdate.festival != null && tdate.festival!.isNotEmpty) { - // 显示节日 subText = tdate.festival; } else if (tdate.solarTerm != null && tdate.solarTerm!.isNotEmpty) { - // 显示节气 subText = tdate.solarTerm; } else if (tdate.lunarInfo != null) { - // 显示农历日期 subText = tdate.lunarInfo!.dayText; } } diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_popup.dart b/tdesign-component/lib/src/components/calendar/t_calendar_popup.dart deleted file mode 100644 index 533d6e09b..000000000 --- a/tdesign-component/lib/src/components/calendar/t_calendar_popup.dart +++ /dev/null @@ -1,221 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import '../../../tdesign_flutter.dart'; - -typedef CalendarBuilder = Widget Function(BuildContext context); - -/// 日历的弹窗模式控制器。 -/// -/// 通过底部滑出弹窗承载 [TCalendar],提供选中态托管、确认/关闭回调与 -/// 全局单例约束。 -class TCalendarPopup { - TCalendarPopup( - 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('[TCalendarPopup] builder or child must be not null'); - } - if (visible == true) { - show(); - } - } - - /// 触发 popup 时的根 context,用于 [Navigator.of] 查找并 push 弹窗路由 - final BuildContext context; - - /// 弹窗顶部距离屏幕顶部的偏移量 - final double? top; - - /// 是否在点击关闭按钮、确认按钮或遮罩层时自动关闭弹窗(默认 true) - final bool? autoClose; - - /// 自定义确认按钮;为 null 时使用默认主色 [TButton] - final Widget? confirmBtn; - - /// 是否在构造时立即调用 [show] 打开弹窗(默认 false) - final bool? visible; - - /// 弹窗关闭后回调 - final VoidCallback? onClose; - - /// 日历构建器,优先级高于 [child] - final CalendarBuilder? builder; - - /// 日历控件,当 [builder] 为 null 时使用 - final TCalendar? child; - - /// 点击确认按钮时回调,参数为当前选中的日期时间戳列表(毫秒) - final void Function(List value)? onConfirm; - - static TSlidePopupRoute? _calendarPopup; - - // 校验 close() 调用方与 show() 是同一实例。 - static TCalendarPopup? _owner; - - // 选中态存储,通过 [TCalendarInherited] 暴露给子树。 - final ValueNotifier> _selected = ValueNotifier>([]); - - bool _closing = false; - - bool get _autoClose => autoClose ?? true; - - /// 当前选中的日期时间戳列表(毫秒) - List get selected => _selected.value; - - /// 打开日历弹窗。 - /// - /// 全局同时只允许一个 [TCalendarPopup] 处于显示状态,重复调用将被忽略 - /// (debug 触发 assert,release 通过 [FlutterError.reportError] 上报)。 - void show() { - assert(_calendarPopup == null, - '[TCalendarPopup] 已有日历弹窗正在显示,请先调用 close() 关闭后再 show()。'); - if (_calendarPopup != null) { - FlutterError.reportError(FlutterErrorDetails( - exception: StateError( - '[TCalendarPopup] show() 被忽略:已有日历弹窗正在显示,' - '请先调用 close() 关闭后再 show()。', - ), - library: 'tdesign_flutter', - context: ErrorDescription('TCalendarPopup.show'), - )); - return; - } - _owner = this; - _calendarPopup = TSlidePopupRoute( - isDismissible: false, - slideTransitionFrom: SlideTransitionFrom.bottom, - modalTop: top, - barrierClick: () { - if (_autoClose) { - close(); - } - }, - builder: (context) { - final built = builder?.call(context); - final childWidget = built ?? child; - if (childWidget == null) { - throw FlutterError( - '[TCalendarPopup] builder 返回 null 且未提供 child,' - '请检查 builder 实现或传入非空 child。', - ); - } - return TCalendarInherited( - selected: _selected, - usePopup: true, - confirmBtn: confirmBtn, - onClose: _onClose, - onConfirm: _onConfirm, - child: childWidget, - ); - }, - ); - Navigator.of(context).push(_calendarPopup!).then((_) { - _deleteRouter(); - }); - } - - void _onClose() { - if (_closing) { - return; - } - if (_autoClose) { - close(); - } - } - - void _onConfirm() { - if (_closing) { - return; - } - onConfirm?.call(List.from(_selected.value)); - if (_autoClose) { - close(); - } - } - - /// 关闭日历弹窗。 - /// - /// 仅当本实例为当前 popup 的 owner 时才会真正 pop;重复调用会被忽略。 - void close() { - final route = _calendarPopup; - if (route == null || _closing) { - return; - } - assert( - _owner == this, - '[TCalendarPopup] close() 被非 owner 实例调用,已忽略。' - '请确保 show() 与 close() 在同一实例上成对调用。', - ); - if (_owner != this) { - return; - } - - _closing = true; - final navigator = route.navigator; - if (navigator != null) { - navigator.pop(); - } else { - Navigator.of(context).pop(); - } - } - - void _deleteRouter() { - _calendarPopup = null; - _owner = null; - _closing = false; - onClose?.call(); - } -} - -class TCalendarInherited extends InheritedWidget { - const TCalendarInherited({ - 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; - - /// 选中态的可写引用(仅供 [TCalendar] 内部更新使用)。 - /// - /// 对外消费方(如自定义 [confirmBtn] 或 [TCalendar.bottom])请使用 - /// [selectedListenable] 这一只读视图。 - final ValueNotifier> selected; - - /// 选中态的只读视图,供下游 widget 监听变化。 - /// - /// ```dart - /// final inherited = TCalendarInherited.of(context); - /// return ValueListenableBuilder>( - /// valueListenable: inherited!.selectedListenable, - /// builder: (ctx, dates, _) => Text('已选 ${dates.length} 天'), - /// ); - /// ``` - ValueListenable> get selectedListenable => selected; - - final bool? usePopup; - final VoidCallback? onConfirm; - final Widget? confirmBtn; - - @override - bool updateShouldNotify(covariant TCalendarInherited oldWidget) { - // 选中态变化由 [selectedListenable] 通知,本 InheritedWidget 自身无需触发重建。 - return false; - } - - static TCalendarInherited? of(BuildContext context) { - return context.dependOnInheritedWidgetOfExactType(); - } -} diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_style.dart b/tdesign-component/lib/src/components/calendar/t_calendar_style.dart index 5a53e1d30..5dba6ab1c 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar_style.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar_style.dart @@ -11,6 +11,7 @@ class TCalendarStyle { this.weekdayStyle, this.monthTitleStyle, this.cellStyle, + this.todayStyle, this.centreColor, this.cellDecoration, this.cellPrefixStyle, diff --git a/tdesign-component/lib/src/components/dialog/t_dialog_widget.dart b/tdesign-component/lib/src/components/dialog/t_dialog_widget.dart index 71fce313b..0bdc608ae 100644 --- a/tdesign-component/lib/src/components/dialog/t_dialog_widget.dart +++ b/tdesign-component/lib/src/components/dialog/t_dialog_widget.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import '../../../tdesign_flutter.dart'; +import 't_dialog.dart'; /// TDialog手脚架 class TDialogScaffold extends StatelessWidget { diff --git a/tdesign-component/lib/src/components/popup/t_popup_panel.dart b/tdesign-component/lib/src/components/popup/t_popup_panel.dart index 9a6f3fa85..9515df905 100644 --- a/tdesign-component/lib/src/components/popup/t_popup_panel.dart +++ b/tdesign-component/lib/src/components/popup/t_popup_panel.dart @@ -18,6 +18,7 @@ abstract class TPopupBasePanel extends StatefulWidget { this.draggable = false, this.maxHeightRatio = 0.9, this.minHeightRatio = 0.3, + this.fixedHeight, }) : super(key: key); /// 子控件 @@ -44,6 +45,10 @@ abstract class TPopupBasePanel extends StatefulWidget { /// 最小高度比例 final double minHeightRatio; + /// 固定高度(px)。设置后忽略 [maxHeightRatio] / [minHeightRatio], + /// 面板高度固定为该值。 + final double? fixedHeight; + @override State createState(); } @@ -58,7 +63,6 @@ abstract class _TPopupBaseState extends State double _maxHeight = 0; double _minHeight = 0; double _currentHeight = 0; - double _lastChildHeight = 0; double _lastScreenHeight = 0; double? _lastMaxHeightRatio; double? _lastMinHeightRatio; @@ -77,68 +81,40 @@ abstract class _TPopupBaseState extends State if (widget.draggable) { _controller.addListener(_updateHeight); } - WidgetsBinding.instance.addPostFrameCallback((_) => _measureChildHeight()); + WidgetsBinding.instance.addPostFrameCallback((_) => _initHeight()); } - /// 测量子组件高度并更新弹窗布局参数 - /// 1.获取子组件渲染尺寸 - /// 2.计算实际需要的基础高度 - /// 3.动态计算计算最大最小高度比例 - /// 4.更新动画控制器状态 - void _measureChildHeight() { - // 获取子组件渲染对象 - final context = _childKey.currentContext; - if (context == null) { - return; - } - final renderBox = context.findRenderObject() as RenderBox?; - if (renderBox == null || !renderBox.hasSize) { - return; - } - - final screenHeight = MediaQuery.of(context).size.height; - final childHeight = renderBox.size.height; + /// 根据屏幕高度和比例(或固定高度)初始化面板高度 + void _initHeight() { + final ctx = _childKey.currentContext ?? context; + final screenHeight = MediaQuery.of(ctx).size.height; - // 如果高度和相关配置没有变化,则不重新计算 - if (_lastChildHeight == childHeight && - _lastScreenHeight == screenHeight && + if (_lastScreenHeight == screenHeight && _lastMaxHeightRatio == widget.maxHeightRatio && _lastMinHeightRatio == widget.minHeightRatio && _lastDraggable == widget.draggable) { return; } - _lastChildHeight = childHeight; _lastScreenHeight = screenHeight; _lastMaxHeightRatio = widget.maxHeightRatio; _lastMinHeightRatio = widget.minHeightRatio; _lastDraggable = widget.draggable; - final headerHeight = widget.draggable ? _headerHeight : _headerHeight; - final baseHeight = _dragHandleHeight + headerHeight + childHeight; - - // 动态计算最大最小高度 - final maxHeightByRatio = screenHeight * widget.maxHeightRatio; - final minHeightByRatio = screenHeight * widget.minHeightRatio; - - // 内容高度和比例约束 - _maxHeight = min(baseHeight, maxHeightByRatio); - _minHeight = max(baseHeight * 0.5, minHeightByRatio); - if (_minHeight > _maxHeight) { - _minHeight = _maxHeight; + if (widget.fixedHeight != null) { + _maxHeight = widget.fixedHeight!; + _minHeight = widget.fixedHeight!; + } else { + _maxHeight = screenHeight * widget.maxHeightRatio; + _minHeight = screenHeight * widget.minHeightRatio; + if (_minHeight > _maxHeight) { + _minHeight = _maxHeight; + } } - // 初始化当前高度 - _currentHeight = baseHeight.clamp(_minHeight, _maxHeight); - // // 同步动画控制器 如果不赋值,会导致“键盘弹出默认遮挡”用例无法展示 - // final newValue = ((_currentHeight - _minHeight) / - // (_maxHeight - _minHeight).clamp(0.1, 1.0)) - // .clamp(0.0, 1.0); - // if ((_controller.value - newValue).abs() > 0.001) { - // _controller.value = newValue; - // } - // 同步动画控制器 - _controller.value = (_currentHeight - _minHeight) / - (_maxHeight - _minHeight).clamp(0.1, 1.0); + _currentHeight = _maxHeight; + _controller.value = 1.0; + // 触发 rebuild 以应用新计算的 _currentHeight + setState(() {}); } void _updateHeight() => setState(() { @@ -207,14 +183,18 @@ abstract class _TPopupBaseState extends State } @override - Widget build(BuildContext context) { + void didChangeDependencies() { + super.didChangeDependencies(); WidgetsBinding.instance.addPostFrameCallback((_) { - // 每次 build 都测量子内容高度,确保内容变化时高度自适应 (拖动时不测量) - if (!_isDragging) { - _measureChildHeight(); + if (!mounted || _isDragging) { + return; } + _initHeight(); }); + } + @override + Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, _) => RepaintBoundary( @@ -232,7 +212,7 @@ abstract class _TPopupBaseState extends State child: Column(children: [ _buildDragHandle(), buildHeader(context), - SizedBox( + Expanded( child: _buildContent(), ), ]), @@ -311,6 +291,7 @@ class TPopupBottomDisplayPanel extends TPopupBasePanel { super.draggable, super.maxHeightRatio, super.minHeightRatio, + super.fixedHeight, }); /// 标题字体大小 diff --git a/tdesign-component/lib/tdesign_flutter.dart b/tdesign-component/lib/tdesign_flutter.dart index 8a6b90375..02701bef7 100644 --- a/tdesign-component/lib/tdesign_flutter.dart +++ b/tdesign-component/lib/tdesign_flutter.dart @@ -17,6 +17,7 @@ export 'src/components/checkbox/t_check_box_group.dart'; export 'src/components/collapse/t_collapse.dart'; export 'src/components/collapse/t_collapse_panel.dart'; export 'src/components/date_time_picker/t_date_time_picker.dart'; +export 'src/components/dialog/t_dialog.dart'; export 'src/components/divider/t_divider.dart'; export 'src/components/drawer/t_drawer.dart'; export 'src/components/drawer/t_drawer_widget.dart'; diff --git a/tdesign-component/test/t_calendar_test.dart b/tdesign-component/test/t_calendar_test.dart index 5d9418dc4..0c22b1cc1 100644 --- a/tdesign-component/test/t_calendar_test.dart +++ b/tdesign-component/test/t_calendar_test.dart @@ -108,38 +108,338 @@ void main() { }); }); - group('TCalendarPopup', () { - testWidgets('已有弹窗时再次 show 不会叠加', (tester) async { - late BuildContext context; + // ----------------------------------------------------------------------- + // 辅助:将 DateTime 转为日毫秒时间戳(去除时分秒) + // ----------------------------------------------------------------------- + int _ms(DateTime d) => + DateTime(d.year, d.month, d.day).millisecondsSinceEpoch; + + // ----------------------------------------------------------------------- + // 单选 + // ----------------------------------------------------------------------- + group('TCalendar — 单选 (single)', () { + testWidgets('点击日期触发 onChange', (tester) async { + final day15 = DateTime(2024, 6, 15); + final day20 = DateTime(2024, 6, 20); + final minMs = _ms(DateTime(2024, 6, 1)); + final maxMs = _ms(DateTime(2024, 6, 30)); + List? result; + await tester.pumpWidget( _buildTestApp( - Builder( - builder: (ctx) { - context = ctx; - return const SizedBox.shrink(); - }, + TCalendar( + height: 640, + type: CalendarType.single, + value: [_ms(day15)], + minDate: minMs, + maxDate: maxMs, + onChange: (v) => result = v, ), ), ); + await tester.pumpAndSettle(); - TCalendarPopup( - context, - child: const TCalendar(title: '日历A', height: 400), - ).show(); + // 点击 20 号 + await tester.tap(find.text('20')); await tester.pump(); - await tester.pump(const Duration(milliseconds: 300)); - expect( - () => TCalendarPopup( - context, - child: const TCalendar(title: '日历B', height: 400), - ).show(), - throwsAssertionError, + expect(result, isNotNull); + expect(result!.length, 1); + expect(result!.first, _ms(day20)); + }); + + testWidgets('点击已选中日期不重复触发 onChange', (tester) async { + final day15 = DateTime(2024, 6, 15); + final minMs = _ms(DateTime(2024, 6, 1)); + final maxMs = _ms(DateTime(2024, 6, 30)); + var callCount = 0; + + await tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + type: CalendarType.single, + value: [_ms(day15)], + minDate: minMs, + maxDate: maxMs, + onChange: (_) => callCount++, + ), + ), ); + await tester.pumpAndSettle(); + + // 点击已选中的 15 号 + await tester.tap(find.text('15')); await tester.pump(); - expect(find.text('日历A'), findsOneWidget); - expect(find.text('日历B'), findsNothing); + expect(callCount, 0); + }); + + testWidgets('点击 disabled 日期不改变选中状态', (tester) async { + final day15 = DateTime(2024, 6, 15); + final minMs = _ms(DateTime(2024, 6, 10)); + final maxMs = _ms(DateTime(2024, 6, 25)); + List? result; + + await tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + type: CalendarType.single, + value: [_ms(day15)], + minDate: minMs, + maxDate: maxMs, + onChange: (v) => result = v, + ), + ), + ); + await tester.pumpAndSettle(); + + // 点击超出范围的 5 号(disabled) + final finder5 = find.text('5'); + if (finder5.evaluate().isNotEmpty) { + await tester.tap(finder5.first); + await tester.pump(); + } + + // onChange 不应被触发 + expect(result, isNull); + }); + }); + + // ----------------------------------------------------------------------- + // 多选 + // ----------------------------------------------------------------------- + group('TCalendar — 多选 (multiple)', () { + testWidgets('点击新日期添加选中', (tester) async { + final day15 = DateTime(2024, 6, 15); + final minMs = _ms(DateTime(2024, 6, 1)); + final maxMs = _ms(DateTime(2024, 6, 30)); + List? result; + + await tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + type: CalendarType.multiple, + value: [_ms(day15)], + minDate: minMs, + maxDate: maxMs, + onChange: (v) => result = v, + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('20')); + await tester.pump(); + + expect(result, isNotNull); + expect(result!.length, 2); + expect(result!.contains(_ms(day15)), isTrue); + expect(result!.contains(_ms(DateTime(2024, 6, 20))), isTrue); + }); + + testWidgets('再次点击已选日期取消选中', (tester) async { + final day15 = DateTime(2024, 6, 15); + final day20 = DateTime(2024, 6, 20); + final minMs = _ms(DateTime(2024, 6, 1)); + final maxMs = _ms(DateTime(2024, 6, 30)); + List? result; + + await tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + type: CalendarType.multiple, + value: [_ms(day15), _ms(day20)], + minDate: minMs, + maxDate: maxMs, + onChange: (v) => result = v, + ), + ), + ); + await tester.pumpAndSettle(); + + // 点击已选中的 15 号取消 + await tester.tap(find.text('15')); + await tester.pump(); + + expect(result, isNotNull); + expect(result!.length, 1); + expect(result!.first, _ms(day20)); + }); + }); + + // ----------------------------------------------------------------------- + // 区间选择 + // ----------------------------------------------------------------------- + group('TCalendar — 区间选择 (range)', () { + testWidgets('选择 start 和 end 触发 onChange', (tester) async { + final minMs = _ms(DateTime(2024, 6, 1)); + final maxMs = _ms(DateTime(2024, 6, 30)); + List? result; + + await tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + type: CalendarType.range, + minDate: minMs, + maxDate: maxMs, + onChange: (v) => result = v, + ), + ), + ); + await tester.pumpAndSettle(); + + // 先点 10 号作为 start + await tester.tap(find.text('10')); + await tester.pump(); + + expect(result, isNotNull); + expect(result!.length, 1); + expect(result!.first, _ms(DateTime(2024, 6, 10))); + + // 再点 20 号作为 end + await tester.tap(find.text('20')); + await tester.pump(); + + expect(result!.length, 2); + expect(result!.first, _ms(DateTime(2024, 6, 10))); + expect(result!.last, _ms(DateTime(2024, 6, 20))); + }); + + testWidgets('end 在 start 之前时重置为新 start', (tester) async { + final day20 = DateTime(2024, 6, 20); + final minMs = _ms(DateTime(2024, 6, 1)); + final maxMs = _ms(DateTime(2024, 6, 30)); + List? result; + + await tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + type: CalendarType.range, + value: [_ms(day20)], + minDate: minMs, + maxDate: maxMs, + onChange: (v) => result = v, + ), + ), + ); + await tester.pumpAndSettle(); + + // 点击 10 号(在 start=20 之前)应重置 + await tester.tap(find.text('10')); + await tester.pump(); + + expect(result, isNotNull); + expect(result!.length, 1); + expect(result!.first, _ms(DateTime(2024, 6, 10))); + }); + }); + + // ----------------------------------------------------------------------- + // 边界条件 + // ----------------------------------------------------------------------- + group('TCalendar — 边界条件', () { + testWidgets('不传 value 时正常渲染', (tester) async { + await tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + type: CalendarType.single, + minDate: _ms(DateTime(2024, 6, 1)), + maxDate: _ms(DateTime(2024, 6, 30)), + ), + ), + ); + await tester.pumpAndSettle(); + + // 应能看到日期 + expect(find.text('1'), findsWidgets); + expect(find.text('30'), findsOneWidget); + }); + + testWidgets('空 value 列表正常渲染', (tester) async { + await tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + type: CalendarType.single, + value: const [], + minDate: _ms(DateTime(2024, 6, 1)), + maxDate: _ms(DateTime(2024, 6, 30)), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('15'), findsOneWidget); + }); + + testWidgets('onCellClick 回调携带正确参数', (tester) async { + final minMs = _ms(DateTime(2024, 6, 1)); + final maxMs = _ms(DateTime(2024, 6, 30)); + int? clickedValue; + DateSelectType? clickedType; + + await tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + type: CalendarType.single, + minDate: minMs, + maxDate: maxMs, + onCellClick: (value, type, tdate) { + clickedValue = value; + clickedType = type; + }, + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('15')); + await tester.pump(); + + expect(clickedValue, _ms(DateTime(2024, 6, 15))); + expect(clickedType, DateSelectType.selected); + }); + + testWidgets('单月范围(minDate == maxDate 同月)正常渲染', (tester) async { + await tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + type: CalendarType.single, + minDate: _ms(DateTime(2024, 6, 1)), + maxDate: _ms(DateTime(2024, 6, 1)), + ), + ), + ); + await tester.pumpAndSettle(); + + // 只有 1 号可选,其余 disabled + expect(find.text('1'), findsWidgets); + }); + + testWidgets('firstDayOfWeek = 1(周一开始)正常渲染', (tester) async { + await tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + type: CalendarType.single, + firstDayOfWeek: 1, + minDate: _ms(DateTime(2024, 6, 1)), + maxDate: _ms(DateTime(2024, 6, 30)), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('15'), findsOneWidget); }); }); } From b095c6f7fd99c90d3c22cb621a133f31ffa2e0fd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 06:45:44 +0000 Subject: [PATCH 14/35] [autofix.ci] apply automated fixes --- .../example/assets/api/calendar_api.md | 62 +- .../example/assets/api/popup_api.md | 1 + .../assets/code/calendar._buildBlock.txt | 2 +- .../assets/code/calendar._buildLunar.txt | 2 - tdesign-site/src/calendar/README.md | 688 +++++++----------- tdesign-site/src/popup/README.md | 1 + 6 files changed, 254 insertions(+), 502 deletions(-) diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md index 4072b7969..77aba4638 100644 --- a/tdesign-component/example/assets/api/calendar_api.md +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -2,14 +2,12 @@ ### TCalendar #### 默认构造方法 -按照日历形式展示数据或日期的容器,支持内嵌和弹窗两种模式。 - | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | anchorDate | DateTime? | - | 锚点日期 | | animateTo | bool? | false | 动画滚动到指定位置 | -| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,以浮层方式叠加在日历主体之上。**仅能在 Popup 内使用。** | -| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。**仅能在 Popup 内使用。** | +| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,以浮层方式叠加在日历主体之上。 | +| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。**仅能在 [TPopupBottomDisplayPanel] 内使用。** | | cellHeight | double? | 60 | 日期高度 | | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | | dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 | @@ -17,7 +15,7 @@ | displayFormat | String? | 'year month' | 年月显示格式,`year`表示年,`month`表示月,如`year month`表示年在前、月在后、中间隔一个空格 | | firstDayOfWeek | int? | 0 | 第一天从星期几开始,默认 0 = 周日 | | format | CalendarFormat? | - | 用于格式化日期的函数,可定义日期前后的显示内容和日期样式 | -| height | double? | - | 高度 | +| height | double? | - | 高度,不传时内嵌模式自动按 5 行日期计算 | | key | | - | | | maxDate | int? | - | 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认 2100-12-31 | | minDate | int? | - | 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认 1970-01-01 | @@ -37,57 +35,12 @@ | value | List? | - | 当前选择的日期(fromMillisecondsSinceEpoch),不传则默认今天,当 type = single 时数组长度为1 | | width | double? | - | 宽度 | -``` -``` -### TCalendar.showPopup #### 静态方法 -弹出日历选择器,返回选中的日期列表。取消或关闭弹窗时返回 `null`;点击确认时返回选中日期的毫秒时间戳列表。 - -```dart -final result = await TCalendar.showPopup( - context, - title: '请选择日期', - type: CalendarType.single, -); -if (result != null) { - print('选中了: $result'); -} -``` - -若需完全自定义布局,请直接使用 `TCalendar` + `TPopupBottomDisplayPanel` + `TSlidePopupRoute` 自行组装。 - -| 参数 | 类型 | 默认值 | 说明 | +| 名称 | 返回类型 | 参数 | 说明 | | --- | --- | --- | --- | -| context | BuildContext | - | 必填,触发 popup 时的 context | -| title | String? | - | 弹窗标题 | -| type | CalendarType | CalendarType.single | 日历选择类型 | -| value | List? | - | 当前选中的日期(毫秒时间戳列表) | -| minDate | int? | - | 最小可选日期(毫秒时间戳),不传则默认 1970-01-01 | -| maxDate | int? | - | 最大可选日期(毫秒时间戳),不传则默认 2100-12-31 | -| anchorDate | DateTime? | - | 锚点日期,弹出时滚动到该月 | -| fixedHeight | double? | - | 面板固定高度(不传时自动计算) | -| firstDayOfWeek | int? | 0 | 第一天从星期几开始,默认 0 = 周日 | -| displayFormat | String? | 'year month' | 年月显示格式 | -| cellHeight | double? | 60 | 日期高度 | -| style | TCalendarStyle? | - | 自定义样式 | -| format | CalendarFormat? | - | 用于格式化日期的函数 | -| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器 | -| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式) | -| confirmBtn | Widget? | - | 自定义确认按钮 | -| onConfirm | void Function(List)? | - | 点击确认的额外回调 | -| onClose | VoidCallback? | - | 弹窗关闭后的回调 | -| onCellClick | void Function(int value, DateSelectType type, TDate tdate)? | - | 点击日期时触发 | -| onCellLongPress | void Function(int value, DateSelectType type, TDate tdate)? | - | 长按日期时触发 | -| autoClose | bool | true | 点击遮罩或物理返回是否关闭 | -| draggable | bool | false | 面板是否可拖动 | -| cellWidget | Widget? Function(BuildContext, TDate, DateSelectType)? | - | 自定义日期单元格组件 | -| dateType | TCalendarDateType | TCalendarDateType.solar | 日历类型:阳历或农历 | -| dataSource | TCalendarDataSource? | - | 外部数据源 | -| showLunarInfo | bool | false | 阳历模式下是否显示农历信息 | -| onMonthChange | ValueChanged? | - | 月份变化时触发 | -| monthTitleBuilder | Widget Function(BuildContext, DateTime)? | - | 月标题构建器 | +| showPopup | | required BuildContext context, String? title, CalendarType type, List? value, int? minDate, int? maxDate, DateTime? anchorDate, double? fixedHeight, int? firstDayOfWeek, String? displayFormat, double? cellHeight, TCalendarStyle? style, CalendarFormat? format, CalendarBottomBuilder? bottom, ValueListenable? bottomExpanded, Widget? confirmBtn, void Function(List)? onConfirm, VoidCallback? onClose, void Function(int value, DateSelectType type, TDate tdate)? onCellClick, void Function(int value, DateSelectType type, TDate tdate)? onCellLongPress, bool autoClose, bool draggable, Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? cellWidget, TCalendarDateType dateType, TCalendarDataSource? dataSource, bool showLunarInfo, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中日期的毫秒时间戳列表。 ```dart final result = await TCalendar.showPopup( context, title: '请选择日期', type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | ``` ``` @@ -122,11 +75,6 @@ if (result != null) { ``` ### TCalendarDataSource - -日历数据源抽象类,用于提供农历、节气、节日、假期等附加信息。 - -继承此类并实现相应方法即可为日历添加农历支持。参考 `LunarDataSourceExample`。 - ``` ``` diff --git a/tdesign-component/example/assets/api/popup_api.md b/tdesign-component/example/assets/api/popup_api.md index 8ad1680cc..e36a6e212 100644 --- a/tdesign-component/example/assets/api/popup_api.md +++ b/tdesign-component/example/assets/api/popup_api.md @@ -38,6 +38,7 @@ | closeColor | Color? | - | 关闭按钮颜色 | | closeSize | double? | - | 关闭按钮图标尺寸 | | draggable | | - | | +| fixedHeight | | - | | | hideClose | bool | false | 是否隐藏关闭按钮 | | key | | - | | | maxHeightRatio | | - | | diff --git a/tdesign-component/example/assets/code/calendar._buildBlock.txt b/tdesign-component/example/assets/code/calendar._buildBlock.txt index 83e36a8fc..48d05c79b 100644 --- a/tdesign-component/example/assets/code/calendar._buildBlock.txt +++ b/tdesign-component/example/assets/code/calendar._buildBlock.txt @@ -39,4 +39,4 @@ Widget _buildBlock(BuildContext context) { ), ], ); -} +} \ No newline at end of file diff --git a/tdesign-component/example/assets/code/calendar._buildLunar.txt b/tdesign-component/example/assets/code/calendar._buildLunar.txt index fc8128d23..0eef4e127 100644 --- a/tdesign-component/example/assets/code/calendar._buildLunar.txt +++ b/tdesign-component/example/assets/code/calendar._buildLunar.txt @@ -1,6 +1,5 @@ Widget _buildLunar(BuildContext context) { - final size = MediaQuery.of(context).size; final dataSource = LunarDataSourceExample(); // 当前月份状态 @@ -254,7 +253,6 @@ Widget _buildLunar(BuildContext context) { showLunarInfo: show, dataSource: dataSource, value: value, - height: size.height * 0.6, onChange: (newValue) { selectedDate.value = newValue; diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md index 175cabc59..d6eb71b8a 100644 --- a/tdesign-site/src/calendar/README.md +++ b/tdesign-site/src/calendar/README.md @@ -34,14 +34,13 @@ Widget _buildSimple(BuildContext context) { ### 1 组件样式 -可以自由定义想要的风格 +自定义文案、按钮、单元格
 Widget _buildStyle(BuildContext context) {
-  final size = MediaQuery.of(context).size;
   const map = {
     1: '初一',
     2: '初二',
@@ -49,324 +48,132 @@ Widget _buildStyle(BuildContext context) {
     14: '情人节',
     15: '元宵节',
   };
-  return TCellGroup(
-    cells: [
-      TCell(
-        title: '自定义文案',
-        arrow: true,
-        onClick: (cell) {
-          TCalendarPopup(
-            context,
-            visible: true,
-            child: TCalendar(
-              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: TTheme.of(context).fontTitleMedium?.size,
-                      height: TTheme.of(context).fontTitleMedium?.height,
-                      fontWeight:
-                          TTheme.of(context).fontTitleMedium?.fontWeight,
-                      color: TTheme.of(context).errorColor6,
-                    );
-                    if (day?.typeNotifier.value == DateSelectType.selected) {
-                      day?.style = day.style
-                          ?.copyWith(color: TTheme.of(context).fontWhColor1);
-                    }
-                  }
-                }
-                return null;
-              },
-            ),
-          );
-        },
-      ),
-      TCell(
-        title: '自定义按钮',
-        arrow: true,
-        onClick: (cell) {
-          late final TCalendarPopup calendar;
-          calendar = TCalendarPopup(
-            context,
-            visible: true,
-            confirmBtn: Padding(
-              padding:
-                  EdgeInsets.symmetric(vertical: TTheme.of(context).spacer16),
-              child: TButton(
-                theme: TButtonTheme.danger,
-                shape: TButtonShape.round,
-                text: 'ok',
-                isBlock: true,
-                size: TButtonSize.large,
-                onTap: () {
-                  print(calendar.selected);
-                  calendar.close();
-                },
-              ),
-            ),
-            child: TCalendar(
-              title: '请选择日期',
-              value: [DateTime.now().millisecondsSinceEpoch],
-              height: size.height * 0.6 + 176,
-            ),
-          );
-        },
-      ),
-      TCell(
-        title: '自定义日期区间',
-        arrow: true,
-        onClick: (cell) {
-          TCalendarPopup(
-            context,
-            visible: true,
-            child: TCalendar(
-              title: '请选择日期',
-              minDate: DateTime(2000, 1, 1).millisecondsSinceEpoch,
-              maxDate: DateTime(3000, 1, 1).millisecondsSinceEpoch,
-              value: [DateTime(2024, 10, 1).millisecondsSinceEpoch],
-              height: size.height * 0.6 + 176,
-            ),
-          );
-        },
-      ),
-    ],
-  );
-}
- -
- - - + final customCellSelected = ValueNotifier>( + [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]); -
-Widget _buildStyle(BuildContext context) {
-  final size = MediaQuery.of(context).size;
-  const map = {
-    1: '初一',
-    2: '初二',
-    3: '初三',
-    14: '情人节',
-    15: '元宵节',
-  };
-  return TCellGroup(
-    cells: [
-      TCell(
-        title: '自定义文案',
-        arrow: true,
-        onClick: (cell) {
-          TCalendarPopup(
-            context,
-            visible: true,
-            child: TCalendar(
-              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: TTheme.of(context).fontTitleMedium?.size,
-                      height: TTheme.of(context).fontTitleMedium?.height,
-                      fontWeight:
-                          TTheme.of(context).fontTitleMedium?.fontWeight,
-                      color: TTheme.of(context).errorColor6,
-                    );
-                    if (day?.typeNotifier.value == DateSelectType.selected) {
-                      day?.style = day.style
-                          ?.copyWith(color: TTheme.of(context).fontWhColor1);
+  return ValueListenableBuilder(
+    valueListenable: customCellSelected,
+    builder: (context, cellValue, _) {
+      final cellDate = DateTime.fromMillisecondsSinceEpoch(cellValue[0]);
+      return TCellGroup(
+        cells: [
+          // 1. 自定义文案(format 回调修改 prefix/suffix/style)
+          TCell(
+            title: '自定义文案',
+            arrow: true,
+            onClick: (cell) {
+              TCalendar.showPopup(
+                context,
+                title: '请选择日期',
+                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: TTheme.of(context).fontTitleMedium?.size,
+                        height: TTheme.of(context).fontTitleMedium?.height,
+                        fontWeight:
+                            TTheme.of(context).fontTitleMedium?.fontWeight,
+                        color: TTheme.of(context).errorColor6,
+                      );
+                      if (day?.typeNotifier.value == DateSelectType.selected) {
+                        day?.style = day.style
+                            ?.copyWith(color: TTheme.of(context).fontWhColor1);
+                      }
                     }
                   }
-                }
-                return null;
-              },
-            ),
-          );
-        },
-      ),
-      TCell(
-        title: '自定义按钮',
-        arrow: true,
-        onClick: (cell) {
-          late final TCalendarPopup calendar;
-          calendar = TCalendarPopup(
-            context,
-            visible: true,
-            confirmBtn: Padding(
-              padding:
-                  EdgeInsets.symmetric(vertical: TTheme.of(context).spacer16),
-              child: TButton(
-                theme: TButtonTheme.danger,
-                shape: TButtonShape.round,
-                text: 'ok',
-                isBlock: true,
-                size: TButtonSize.large,
-                onTap: () {
-                  print(calendar.selected);
-                  calendar.close();
+                  return null;
                 },
-              ),
-            ),
-            child: TCalendar(
-              title: '请选择日期',
-              value: [DateTime.now().millisecondsSinceEpoch],
-              height: size.height * 0.6 + 176,
-            ),
-          );
-        },
-      ),
-      TCell(
-        title: '自定义日期区间',
-        arrow: true,
-        onClick: (cell) {
-          TCalendarPopup(
-            context,
-            visible: true,
-            child: TCalendar(
-              title: '请选择日期',
-              minDate: DateTime(2000, 1, 1).millisecondsSinceEpoch,
-              maxDate: DateTime(3000, 1, 1).millisecondsSinceEpoch,
-              value: [DateTime(2024, 10, 1).millisecondsSinceEpoch],
-              height: size.height * 0.6 + 176,
-            ),
-          );
-        },
-      ),
-    ],
-  );
-}
- -
- - -自定义日期单元格 + ); + }, + ), - - + // 2. 自定义确认按钮 + TCell( + title: '自定义按钮', + arrow: true, + onClick: (cell) { + TCalendar.showPopup( + context, + title: '请选择日期', + value: [DateTime.now().millisecondsSinceEpoch], + confirmBtn: Padding( + padding: EdgeInsets.symmetric( + vertical: TTheme.of(context).spacer16), + child: const TButton( + theme: TButtonTheme.danger, + shape: TButtonShape.round, + text: 'ok', + isBlock: true, + size: TButtonSize.large, + ), + ), + onConfirm: (value) => print('confirmed: $value'), + ); + }, + ), -
-Widget _buildCustomCell(BuildContext context) {
-  final size = MediaQuery.of(context).size;
-  final selected = ValueNotifier>(
-      [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]);
-  return ValueListenableBuilder(
-    valueListenable: selected,
-    builder: (context, value, child) {
-      final date = DateTime.fromMillisecondsSinceEpoch(value[0]);
-      return TCellGroup(
-        cells: [
+          // 3. 自定义日期单元格(cellWidget 回调)
           TCell(
             title: '自定义日期单元格',
             arrow: true,
-            note: '${date.year}-${date.month}-${date.day}',
+            note: '${cellDate.year}-${cellDate.month}-${cellDate.day}',
             onClick: (cell) {
-              TCalendarPopup(
+              TCalendar.showPopup(
                 context,
-                visible: true,
-                onConfirm: (value) {
-                  print('onConfirm:$value');
-                  selected.value = value;
-                },
-                onClose: () {
-                  print('onClose');
-                },
-                child: TCalendar(
-                    title: '请选择日期',
-                    value: value,
-                    cellHeight: 80,
-                    height: size.height * 0.6 + 176,
-                    onCellClick: (value, type, tdate) {
-                      print('onCellClick: $value');
-                    },
-                    onCellLongPress: (value, type, tdate) {
-                      print('onCellLongPress: $value');
-                    },
-                    onHeaderClick: (index, week) {
-                      print('onHeaderClick: $week');
-                    },
-                    onChange: (value) {
-                      print('onChange: $value');
-                    },
-                    cellWidget: (context, tdate, selectType) {
-                      final today = DateTime.now();
-                      //当前日期的自定义实现
-                      if (tdate.date.millisecondsSinceEpoch ==
-                              DateTime(today.year, today.month, today.day)
-                                  .millisecondsSinceEpoch &&
-                          selectType != DateSelectType.selected) {
-                        return Container(
-                          decoration: BoxDecoration(
-                            color: TTheme.of(context).brandColor4,
-                            borderRadius: BorderRadius.all(Radius.circular(6)),
-                          ),
-                          constraints: const BoxConstraints(
-                              minWidth: 0, // 最小宽度为0
-                              maxWidth: double.infinity, // 最大宽度无限
-                              minHeight: 0, // 最小高度为0
-                              maxHeight: double.infinity),
-                          child: const Column(
-                            mainAxisAlignment: MainAxisAlignment.center,
-                            children: [
-                              Text('今天',
-                                  style: TextStyle(
-                                      fontSize: 18,
-                                      fontWeight: FontWeight.bold,
-                                      color: Colors.white)),
-                            ],
-                          ),
-                        );
-                      }
-                      if (selectType == DateSelectType.selected) {
-                        return Container(
-                          decoration: BoxDecoration(
-                            color: TTheme.of(context).successColor8,
-                            borderRadius: BorderRadius.all(Radius.circular(6)),
-                          ),
-                          constraints: const BoxConstraints(
-                              minWidth: 0, // 最小宽度为0
-                              maxWidth: double.infinity, // 最大宽度无限
-                              minHeight: 0, // 最小高度为0
-                              maxHeight: double.infinity),
-                          child: Column(
-                            mainAxisAlignment: MainAxisAlignment.center,
-                            children: [
-                              Text('${tdate.date.day}',
-                                  style: const TextStyle(
-                                      fontSize: 18,
-                                      fontWeight: FontWeight.bold,
-                                      color: Colors.white)),
-                              const Text('文案文案',
-                                  style: TextStyle(
-                                      fontSize: 6, color: Colors.white)),
-                              const Text('自定义',
-                                  style: TextStyle(
-                                      fontSize: 12, color: Colors.white)),
-                            ],
-                          ),
-                        );
-                      }
-                      return Column(
+                title: '请选择日期',
+                value: cellValue,
+                cellHeight: 80,
+                onConfirm: (value) => customCellSelected.value = value,
+                cellWidget: (context, tdate, selectType) {
+                  final today = DateTime.now();
+                  final isToday = tdate.date.millisecondsSinceEpoch ==
+                      DateTime(today.year, today.month, today.day)
+                          .millisecondsSinceEpoch;
+
+                  if (isToday && selectType != DateSelectType.selected) {
+                    return _CustomCellContainer(
+                      color: TTheme.of(context).brandColor4,
+                      child: const Text('今天',
+                          style: TextStyle(
+                              fontSize: 18,
+                              fontWeight: FontWeight.bold,
+                              color: Colors.white)),
+                    );
+                  }
+                  if (selectType == DateSelectType.selected) {
+                    return _CustomCellContainer(
+                      color: TTheme.of(context).successColor8,
+                      child: Column(
                         mainAxisAlignment: MainAxisAlignment.center,
                         children: [
                           Text('${tdate.date.day}',
                               style: const TextStyle(
-                                  fontSize: 18, fontWeight: FontWeight.bold)),
-                          const Text('文案文案', style: TextStyle(fontSize: 8)),
-                          const Text('自定义', style: TextStyle(fontSize: 8)),
+                                  fontSize: 18,
+                                  fontWeight: FontWeight.bold,
+                                  color: Colors.white)),
+                          const Text('已选',
+                              style:
+                                  TextStyle(fontSize: 10, color: Colors.white)),
                         ],
-                      );
-                    }),
+                      ),
+                    );
+                  }
+                  return Column(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      Text('${tdate.date.day}',
+                          style: const TextStyle(
+                              fontSize: 18, fontWeight: FontWeight.bold)),
+                      const Text('自定义', style: TextStyle(fontSize: 8)),
+                    ],
+                  );
+                },
               );
             },
           ),
@@ -383,117 +190,140 @@ Widget _buildCustomCell(BuildContext context) {
 
 
   
-Widget _buildCustomCell(BuildContext context) {
-  final size = MediaQuery.of(context).size;
-  final selected = ValueNotifier>(
+Widget _buildStyle(BuildContext context) {
+  const map = {
+    1: '初一',
+    2: '初二',
+    3: '初三',
+    14: '情人节',
+    15: '元宵节',
+  };
+
+  final customCellSelected = ValueNotifier>(
       [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]);
+
   return ValueListenableBuilder(
-    valueListenable: selected,
-    builder: (context, value, child) {
-      final date = DateTime.fromMillisecondsSinceEpoch(value[0]);
+    valueListenable: customCellSelected,
+    builder: (context, cellValue, _) {
+      final cellDate = DateTime.fromMillisecondsSinceEpoch(cellValue[0]);
       return TCellGroup(
         cells: [
+          // 1. 自定义文案(format 回调修改 prefix/suffix/style)
           TCell(
-            title: '自定义日期单元格',
+            title: '自定义文案',
             arrow: true,
-            note: '${date.year}-${date.month}-${date.day}',
             onClick: (cell) {
-              TCalendarPopup(
+              TCalendar.showPopup(
                 context,
-                visible: true,
-                onConfirm: (value) {
-                  print('onConfirm:$value');
-                  selected.value = value;
-                },
-                onClose: () {
-                  print('onClose');
-                },
-                child: TCalendar(
-                    title: '请选择日期',
-                    value: value,
-                    cellHeight: 80,
-                    height: size.height * 0.6 + 176,
-                    onCellClick: (value, type, tdate) {
-                      print('onCellClick: $value');
-                    },
-                    onCellLongPress: (value, type, tdate) {
-                      print('onCellLongPress: $value');
-                    },
-                    onHeaderClick: (index, week) {
-                      print('onHeaderClick: $week');
-                    },
-                    onChange: (value) {
-                      print('onChange: $value');
-                    },
-                    cellWidget: (context, tdate, selectType) {
-                      final today = DateTime.now();
-                      //当前日期的自定义实现
-                      if (tdate.date.millisecondsSinceEpoch ==
-                              DateTime(today.year, today.month, today.day)
-                                  .millisecondsSinceEpoch &&
-                          selectType != DateSelectType.selected) {
-                        return Container(
-                          decoration: BoxDecoration(
-                            color: TTheme.of(context).brandColor4,
-                            borderRadius: BorderRadius.all(Radius.circular(6)),
-                          ),
-                          constraints: const BoxConstraints(
-                              minWidth: 0, // 最小宽度为0
-                              maxWidth: double.infinity, // 最大宽度无限
-                              minHeight: 0, // 最小高度为0
-                              maxHeight: double.infinity),
-                          child: const Column(
-                            mainAxisAlignment: MainAxisAlignment.center,
-                            children: [
-                              Text('今天',
-                                  style: TextStyle(
-                                      fontSize: 18,
-                                      fontWeight: FontWeight.bold,
-                                      color: Colors.white)),
-                            ],
-                          ),
-                        );
-                      }
-                      if (selectType == DateSelectType.selected) {
-                        return Container(
-                          decoration: BoxDecoration(
-                            color: TTheme.of(context).successColor8,
-                            borderRadius: BorderRadius.all(Radius.circular(6)),
-                          ),
-                          constraints: const BoxConstraints(
-                              minWidth: 0, // 最小宽度为0
-                              maxWidth: double.infinity, // 最大宽度无限
-                              minHeight: 0, // 最小高度为0
-                              maxHeight: double.infinity),
-                          child: Column(
-                            mainAxisAlignment: MainAxisAlignment.center,
-                            children: [
-                              Text('${tdate.date.day}',
-                                  style: const TextStyle(
-                                      fontSize: 18,
-                                      fontWeight: FontWeight.bold,
-                                      color: Colors.white)),
-                              const Text('文案文案',
-                                  style: TextStyle(
-                                      fontSize: 6, color: Colors.white)),
-                              const Text('自定义',
-                                  style: TextStyle(
-                                      fontSize: 12, color: Colors.white)),
-                            ],
-                          ),
-                        );
+                title: '请选择日期',
+                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: TTheme.of(context).fontTitleMedium?.size,
+                        height: TTheme.of(context).fontTitleMedium?.height,
+                        fontWeight:
+                            TTheme.of(context).fontTitleMedium?.fontWeight,
+                        color: TTheme.of(context).errorColor6,
+                      );
+                      if (day?.typeNotifier.value == DateSelectType.selected) {
+                        day?.style = day.style
+                            ?.copyWith(color: TTheme.of(context).fontWhColor1);
                       }
-                      return Column(
+                    }
+                  }
+                  return null;
+                },
+              );
+            },
+          ),
+
+          // 2. 自定义确认按钮
+          TCell(
+            title: '自定义按钮',
+            arrow: true,
+            onClick: (cell) {
+              TCalendar.showPopup(
+                context,
+                title: '请选择日期',
+                value: [DateTime.now().millisecondsSinceEpoch],
+                confirmBtn: Padding(
+                  padding: EdgeInsets.symmetric(
+                      vertical: TTheme.of(context).spacer16),
+                  child: const TButton(
+                    theme: TButtonTheme.danger,
+                    shape: TButtonShape.round,
+                    text: 'ok',
+                    isBlock: true,
+                    size: TButtonSize.large,
+                  ),
+                ),
+                onConfirm: (value) => print('confirmed: $value'),
+              );
+            },
+          ),
+
+          // 3. 自定义日期单元格(cellWidget 回调)
+          TCell(
+            title: '自定义日期单元格',
+            arrow: true,
+            note: '${cellDate.year}-${cellDate.month}-${cellDate.day}',
+            onClick: (cell) {
+              TCalendar.showPopup(
+                context,
+                title: '请选择日期',
+                value: cellValue,
+                cellHeight: 80,
+                onConfirm: (value) => customCellSelected.value = value,
+                cellWidget: (context, tdate, selectType) {
+                  final today = DateTime.now();
+                  final isToday = tdate.date.millisecondsSinceEpoch ==
+                      DateTime(today.year, today.month, today.day)
+                          .millisecondsSinceEpoch;
+
+                  if (isToday && selectType != DateSelectType.selected) {
+                    return _CustomCellContainer(
+                      color: TTheme.of(context).brandColor4,
+                      child: const Text('今天',
+                          style: TextStyle(
+                              fontSize: 18,
+                              fontWeight: FontWeight.bold,
+                              color: Colors.white)),
+                    );
+                  }
+                  if (selectType == DateSelectType.selected) {
+                    return _CustomCellContainer(
+                      color: TTheme.of(context).successColor8,
+                      child: Column(
                         mainAxisAlignment: MainAxisAlignment.center,
                         children: [
                           Text('${tdate.date.day}',
                               style: const TextStyle(
-                                  fontSize: 18, fontWeight: FontWeight.bold)),
-                          const Text('文案文案', style: TextStyle(fontSize: 8)),
-                          const Text('自定义', style: TextStyle(fontSize: 8)),
+                                  fontSize: 18,
+                                  fontWeight: FontWeight.bold,
+                                  color: Colors.white)),
+                          const Text('已选',
+                              style:
+                                  TextStyle(fontSize: 10, color: Colors.white)),
                         ],
-                      );
-                    }),
+                      ),
+                    );
+                  }
+                  return Column(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      Text('${tdate.date.day}',
+                          style: const TextStyle(
+                              fontSize: 18, fontWeight: FontWeight.bold)),
+                      const Text('自定义', style: TextStyle(fontSize: 8)),
+                    ],
+                  );
+                },
               );
             },
           ),
@@ -513,16 +343,13 @@ Widget _buildCustomCell(BuildContext context) {
 
   
 Widget _buildBlock(BuildContext context) {
-  final size = MediaQuery.of(context).size;
   final selected = ValueNotifier>(
     [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000],
   );
   return Column(
-    // spacing: TTheme.of(context).spacer16,
     crossAxisAlignment: CrossAxisAlignment.start,
     children: [
       Row(
-        // spacing: TTheme.of(context).spacer16,
         mainAxisAlignment: MainAxisAlignment.center,
         children: [
           TButton(
@@ -549,10 +376,7 @@ Widget _buildBlock(BuildContext context) {
           return TCalendar(
             title: '请选择日期',
             value: value,
-            height: size.height * 0.6 + 176,
             animateTo: true,
-            // 不使用popup时,useSafeArea无效
-            useSafeArea: true,
           );
         },
       ),
@@ -568,16 +392,13 @@ Widget _buildBlock(BuildContext context) {
 
   
 Widget _buildBlock(BuildContext context) {
-  final size = MediaQuery.of(context).size;
   final selected = ValueNotifier>(
     [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000],
   );
   return Column(
-    // spacing: TTheme.of(context).spacer16,
     crossAxisAlignment: CrossAxisAlignment.start,
     children: [
       Row(
-        // spacing: TTheme.of(context).spacer16,
         mainAxisAlignment: MainAxisAlignment.center,
         children: [
           TButton(
@@ -604,10 +425,7 @@ Widget _buildBlock(BuildContext context) {
           return TCalendar(
             title: '请选择日期',
             value: value,
-            height: size.height * 0.6 + 176,
             animateTo: true,
-            // 不使用popup时,useSafeArea无效
-            useSafeArea: true,
           );
         },
       ),
@@ -625,7 +443,6 @@ Widget _buildBlock(BuildContext context) {
 
   
 Widget _buildLunar(BuildContext context) {
-  final size = MediaQuery.of(context).size;
   final dataSource = LunarDataSourceExample();
   
   // 当前月份状态
@@ -710,8 +527,8 @@ Widget _buildLunar(BuildContext context) {
                                 height: 300,
                                 child: Column(
                                   children: [
-                                    Padding(
-                                      padding: const EdgeInsets.all(16),
+                                    const Padding(
+                                      padding: EdgeInsets.all(16),
                                       child: Text(
                                         '选择年份',
                                         style: TextStyle(
@@ -766,8 +583,8 @@ Widget _buildLunar(BuildContext context) {
                                 height: 400,
                                 child: Column(
                                   children: [
-                                    Padding(
-                                      padding: const EdgeInsets.all(16),
+                                    const Padding(
+                                      padding: EdgeInsets.all(16),
                                       child: Text(
                                         '选择月份',
                                         style: TextStyle(
@@ -879,7 +696,6 @@ Widget _buildLunar(BuildContext context) {
                 showLunarInfo: show,
                 dataSource: dataSource,
                 value: value,
-                height: size.height * 0.6,
                 onChange: (newValue) {
                   selectedDate.value = newValue;
                   
@@ -936,7 +752,6 @@ Widget _buildLunar(BuildContext context) {
 
   
 Widget _buildLunar(BuildContext context) {
-  final size = MediaQuery.of(context).size;
   final dataSource = LunarDataSourceExample();
   
   // 当前月份状态
@@ -1021,8 +836,8 @@ Widget _buildLunar(BuildContext context) {
                                 height: 300,
                                 child: Column(
                                   children: [
-                                    Padding(
-                                      padding: const EdgeInsets.all(16),
+                                    const Padding(
+                                      padding: EdgeInsets.all(16),
                                       child: Text(
                                         '选择年份',
                                         style: TextStyle(
@@ -1077,8 +892,8 @@ Widget _buildLunar(BuildContext context) {
                                 height: 400,
                                 child: Column(
                                   children: [
-                                    Padding(
-                                      padding: const EdgeInsets.all(16),
+                                    const Padding(
+                                      padding: EdgeInsets.all(16),
                                       child: Text(
                                         '选择月份',
                                         style: TextStyle(
@@ -1190,7 +1005,6 @@ Widget _buildLunar(BuildContext context) {
                 showLunarInfo: show,
                 dataSource: dataSource,
                 value: value,
-                height: size.height * 0.6,
                 onChange: (newValue) {
                   selectedDate.value = newValue;
                   
@@ -1252,7 +1066,7 @@ Widget _buildLunar(BuildContext context) {
 | anchorDate | DateTime? | - | 锚点日期 |
 | animateTo | bool? | false | 动画滚动到指定位置 |
 | bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,以浮层方式叠加在日历主体之上。 |
-| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。**仅能在 [TCalendarPopup] 内使用。** |
+| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。**仅能在 [TPopupBottomDisplayPanel] 内使用。** |
 | cellHeight | double? | 60 | 日期高度 |
 | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 |
 | dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 |
@@ -1260,10 +1074,10 @@ Widget _buildLunar(BuildContext context) {
 | displayFormat | String? | 'year month' | 年月显示格式,`year`表示年,`month`表示月,如`year month`表示年在前、月在后、中间隔一个空格 |
 | firstDayOfWeek | int? | 0 | 第一天从星期几开始,默认 0 = 周日 |
 | format | CalendarFormat? | - | 用于格式化日期的函数,可定义日期前后的显示内容和日期样式 |
-| height | double? | - | 高度 |
+| height | double? | - | 高度,不传时内嵌模式自动按 5 行日期计算 |
 | key |  | - |  |
-| maxDate | int? | - | 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认半年后 |
-| minDate | int? | - | 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认今天 |
+| maxDate | int? | - | 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认 2100-12-31 |
+| minDate | int? | - | 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认 1970-01-01 |
 | monthTitleBuilder | Widget Function(BuildContext context, DateTime monthDate)? | - | 月标题构建器 |
 | monthTitleHeight | double? | 22 | 月标题高度 |
 | onCellClick | void Function(int value, DateSelectType type, TDate tdate)? | - | 点击日期时触发 |
@@ -1280,23 +1094,12 @@ Widget _buildLunar(BuildContext context) {
 | value | List? | - | 当前选择的日期(fromMillisecondsSinceEpoch),不传则默认今天,当 type = single 时数组长度为1 |
 | width | double? | - | 宽度 |
 
-```
-```
 
-### TCalendarPopup
-#### 默认构造方法
+#### 静态方法
 
-| 参数 | 类型 | 默认值 | 说明 |
+| 名称 | 返回类型 | 参数 | 说明 |
 | --- | --- | --- | --- |
-| autoClose | bool? | true | 是否在点击关闭按钮、确认按钮或遮罩层时自动关闭弹窗(默认 true) |
-| builder | CalendarBuilder? | - | 日历构建器,优先级高于 [child] |
-| child | TCalendar? | - | 日历控件,当 [builder] 为 null 时使用 |
-| confirmBtn | Widget? | - | 自定义确认按钮;为 null 时使用默认主色 [TButton] |
-| context | BuildContext | context | 触发 popup 时的根 context,用于 [Navigator.of] 查找并 push 弹窗路由 |
-| onClose | VoidCallback? | - | 弹窗关闭后回调 |
-| onConfirm | void Function(List value)? | - | 点击确认按钮时回调,参数为当前选中的日期时间戳列表(毫秒) |
-| top | double? | - | 弹窗顶部距离屏幕顶部的偏移量 |
-| visible | bool? | - | 是否在构造时立即调用 [show] 打开弹窗(默认 false) |
+| showPopup |  |   required BuildContext context,  String? title,  CalendarType type,  List? value,  int? minDate,  int? maxDate,  DateTime? anchorDate,  double? fixedHeight,  int? firstDayOfWeek,  String? displayFormat,  double? cellHeight,  TCalendarStyle? style,  CalendarFormat? format,  CalendarBottomBuilder? bottom,  ValueListenable? bottomExpanded,  Widget? confirmBtn,  void Function(List)? onConfirm,  VoidCallback? onClose,  void Function(int value, DateSelectType type, TDate tdate)? onCellClick,  void Function(int value, DateSelectType type, TDate tdate)? onCellLongPress,  bool autoClose,  bool draggable,  Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? cellWidget,  TCalendarDateType dateType,  TCalendarDataSource? dataSource,  bool showLunarInfo,  ValueChanged? onMonthChange,  Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。     取消或关闭弹窗时返回 `null`;点击确认时返回选中日期的毫秒时间戳列表。     ```dart   final result = await TCalendar.showPopup(     context,     title: '请选择日期',     type: CalendarType.single,   );   if (result != null) {     print('选中了: $result');   }   ```     若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel]   + [TSlidePopupRoute] 自行组装。 |
 
 ```
 ```
@@ -1316,6 +1119,7 @@ Widget _buildLunar(BuildContext context) {
 | titleCloseColor | Color? | - | header区域 关闭图标的颜色 |
 | titleMaxLine | int? | - | header区域 [TCalendar.title]的行数 |
 | titleStyle | TextStyle? | - | header区域 [TCalendar.title]的样式 |
+| todayStyle | TextStyle? | - | 当天日期样式 |
 | weekdayStyle | TextStyle? | - | header区域 周 文字样式 |
 
 
diff --git a/tdesign-site/src/popup/README.md b/tdesign-site/src/popup/README.md
index ec55fb953..9c220755c 100644
--- a/tdesign-site/src/popup/README.md
+++ b/tdesign-site/src/popup/README.md
@@ -498,6 +498,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | closeColor | Color? | - | 关闭按钮颜色 |
 | closeSize | double? | - | 关闭按钮图标尺寸 |
 | draggable |  | - |  |
+| fixedHeight |  | - |  |
 | hideClose | bool | false | 是否隐藏关闭按钮 |
 | key |  | - |  |
 | maxHeightRatio |  | - |  |

From 758d053b4ba234fbaa6c766735907eb13d91bfcc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= 
Date: Tue, 19 May 2026 18:50:19 +0800
Subject: [PATCH 15/35] =?UTF-8?q?refactor(calendar):=20=E9=87=8D=E6=9E=84A?=
 =?UTF-8?q?PI=E4=BD=BF=E7=94=A8DateTime=E6=9B=BF=E4=BB=A3=E6=97=B6?=
 =?UTF-8?q?=E9=97=B4=E6=88=B3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../assets/code/calendar._buildBlock.txt      |  42 -
 .../assets/code/calendar._buildStyle.txt      |  84 +-
 .../example/lib/page/t_calendar_page.dart     | 930 +++++++++---------
 .../src/components/calendar/t_calendar.dart   | 402 ++++----
 .../components/calendar/t_calendar_body.dart  |  20 +-
 .../components/calendar/t_calendar_cell.dart  |  46 +-
 .../calendar/t_calendar_header.dart           |  36 +-
 .../components/calendar/t_calendar_style.dart |   4 +-
 .../lib/src/theme/resource_delegate.dart      |  40 +-
 9 files changed, 776 insertions(+), 828 deletions(-)
 delete mode 100644 tdesign-component/example/assets/code/calendar._buildBlock.txt

diff --git a/tdesign-component/example/assets/code/calendar._buildBlock.txt b/tdesign-component/example/assets/code/calendar._buildBlock.txt
deleted file mode 100644
index 48d05c79b..000000000
--- a/tdesign-component/example/assets/code/calendar._buildBlock.txt
+++ /dev/null
@@ -1,42 +0,0 @@
-
-Widget _buildBlock(BuildContext context) {
-  final selected = ValueNotifier>(
-    [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000],
-  );
-  return Column(
-    crossAxisAlignment: CrossAxisAlignment.start,
-    children: [
-      Row(
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: [
-          TButton(
-            text: '加一个月',
-            theme: TButtonTheme.primary,
-            onTap: () {
-              selected.value = [selected.value[0] + 30 * 24 * 60 * 60 * 1000];
-            },
-          ),
-          const SizedBox(width: 16),
-          TButton(
-            text: '减一个月',
-            theme: TButtonTheme.primary,
-            onTap: () {
-              selected.value = [selected.value[0] - 30 * 24 * 60 * 60 * 1000];
-            },
-          ),
-        ],
-      ),
-      const SizedBox(height: 16),
-      ValueListenableBuilder(
-        valueListenable: selected,
-        builder: (context, value, child) {
-          return TCalendar(
-            title: '请选择日期',
-            value: value,
-            animateTo: true,
-          );
-        },
-      ),
-    ],
-  );
-}
\ No newline at end of file
diff --git a/tdesign-component/example/assets/code/calendar._buildStyle.txt b/tdesign-component/example/assets/code/calendar._buildStyle.txt
index 69d12f6e5..f5ef2f4f1 100644
--- a/tdesign-component/example/assets/code/calendar._buildStyle.txt
+++ b/tdesign-component/example/assets/code/calendar._buildStyle.txt
@@ -8,45 +8,62 @@ Widget _buildStyle(BuildContext context) {
     15: '元宵节',
   };
 
-  final customCellSelected = ValueNotifier>(
-      [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]);
+  final customCellSelected = ValueNotifier>(
+      [DateTime.now().add(const Duration(days: 30))]);
 
   return ValueListenableBuilder(
     valueListenable: customCellSelected,
     builder: (context, cellValue, _) {
-      final cellDate = DateTime.fromMillisecondsSinceEpoch(cellValue[0]);
+      final cellDate = cellValue[0];
       return TCellGroup(
         cells: [
-          // 1. 自定义文案(format 回调修改 prefix/suffix/style)
+          // 1. 自定义文案(cellWidget 回调自定义 cell 渲染)
           TCell(
             title: '自定义文案',
             arrow: true,
             onClick: (cell) {
               TCalendar.showPopup(
                 context,
-                title: '请选择日期',
-                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: TTheme.of(context).fontTitleMedium?.size,
-                        height: TTheme.of(context).fontTitleMedium?.height,
-                        fontWeight:
-                            TTheme.of(context).fontTitleMedium?.fontWeight,
-                        color: TTheme.of(context).errorColor6,
-                      );
-                      if (day?.typeNotifier.value == DateSelectType.selected) {
-                        day?.style = day.style
-                            ?.copyWith(color: TTheme.of(context).fontWhColor1);
-                      }
-                    }
-                  }
-                  return null;
+                titleWidget: const Text('请选择日期'),
+                minDate: DateTime(2022, 1, 1),
+                maxDate: DateTime(2022, 2, 15),
+                cellWidget: (context, tdate, selectType) {
+                  final isSpecial = tdate.date.month == 2 &&
+                      map.keys.contains(tdate.date.day);
+                  final suffix = isSpecial ? '¥100' : '¥60';
+                  final prefix = isSpecial ? map[tdate.date.day] : null;
+                  return Column(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      if (prefix != null)
+                        Text(prefix,
+                            style: TextStyle(
+                              fontSize: 9,
+                              color: isSpecial
+                                  ? TTheme.of(context).errorColor6
+                                  : null,
+                            )),
+                      Text(
+                        tdate.date.day.toString(),
+                        style: TextStyle(
+                          color: selectType == DateSelectType.selected
+                              ? TTheme.of(context).fontWhColor1
+                              : isSpecial
+                                  ? TTheme.of(context).errorColor6
+                                  : null,
+                        ),
+                      ),
+                      Text(suffix,
+                          style: TextStyle(
+                            fontSize: 9,
+                            color: selectType == DateSelectType.selected
+                                ? TTheme.of(context).fontWhColor1
+                                : isSpecial
+                                    ? TTheme.of(context).errorColor6
+                                    : null,
+                          )),
+                    ],
+                  );
                 },
               );
             },
@@ -59,8 +76,8 @@ Widget _buildStyle(BuildContext context) {
             onClick: (cell) {
               TCalendar.showPopup(
                 context,
-                title: '请选择日期',
-                value: [DateTime.now().millisecondsSinceEpoch],
+                titleWidget: const Text('请选择日期'),
+                initialValue: [DateTime.now()],
                 confirmBtn: Padding(
                   padding: EdgeInsets.symmetric(
                       vertical: TTheme.of(context).spacer16),
@@ -85,15 +102,14 @@ Widget _buildStyle(BuildContext context) {
             onClick: (cell) {
               TCalendar.showPopup(
                 context,
-                title: '请选择日期',
-                value: cellValue,
+                titleWidget: const Text('请选择日期'),
+                initialValue: cellValue,
                 cellHeight: 80,
                 onConfirm: (value) => customCellSelected.value = value,
                 cellWidget: (context, tdate, selectType) {
                   final today = DateTime.now();
-                  final isToday = tdate.date.millisecondsSinceEpoch ==
-                      DateTime(today.year, today.month, today.day)
-                          .millisecondsSinceEpoch;
+                  final isToday = tdate.date ==
+                      DateTime(today.year, today.month, today.day);
 
                   if (isToday && selectType != DateSelectType.selected) {
                     return _CustomCellContainer(
diff --git a/tdesign-component/example/lib/page/t_calendar_page.dart b/tdesign-component/example/lib/page/t_calendar_page.dart
index 684871fba..ab763a9bc 100644
--- a/tdesign-component/example/lib/page/t_calendar_page.dart
+++ b/tdesign-component/example/lib/page/t_calendar_page.dart
@@ -11,8 +11,7 @@ import '../lunar_data_source_example.dart';
 ///   - 单选、多选、区间选择
 ///   - 单选 + 时间、区间 + 时间
 ///   - 锚点定位
-/// - **内嵌模式**:直接嵌入页面布局
-/// - **自定义样式**:文案、按钮、日期区间、日期单元格
+/// - **自定义样式**:文案、按钮、日期单元格
 /// - **农历日历**:结合 [TCalendarDataSource] 展示农历信息
 class TCalendarPage extends StatelessWidget {
   const TCalendarPage({super.key});
@@ -42,14 +41,6 @@ class TCalendarPage extends StatelessWidget {
               return const CodeWrapper(builder: _buildStyle);
             },
           ),
-          ExampleItem(
-            desc: '不使用Popup',
-            ignoreCode: true,
-            center: false,
-            builder: (BuildContext context) {
-              return const CodeWrapper(builder: _buildBlock);
-            },
-          ),
           ExampleItem(
             desc: '农历日历',
             ignoreCode: true,
@@ -100,7 +91,7 @@ class _SimpleDemo extends StatelessWidget {
 // ========================= 1. 单选 + 天气 =========================
 /// 单选日历 + bottom 天气面板
 ///
-/// 演示 [TCalendar.showPopup] 的 bottom / bottomExpanded 用法:
+/// 演示 [TCalendar.showPopup] 的 popupBottomBuilder / popupBottomExpanded 用法:
 /// - 选中日期后展开 bottom 区域显示天气信息
 /// - 确认后回传选中值
 class _SingleCalendarCell extends StatefulWidget {
@@ -110,7 +101,7 @@ class _SingleCalendarCell extends StatefulWidget {
 }
 
 class _SingleCalendarCellState extends State<_SingleCalendarCell> {
-  List _selected = const [];
+  List _selected = const [];
   final ValueNotifier _expanded = ValueNotifier(false);
   final Map _cache = {};
 
@@ -135,14 +126,12 @@ class _SingleCalendarCellState extends State<_SingleCalendarCell> {
         _expanded.value = _selected.isNotEmpty;
         TCalendar.showPopup(
           context,
-          title: '请选择日期',
-          value: _selected,
-          bottomExpanded: _expanded,
-          onCellClick: (value, type, tdate) => _expanded.value = true,
-          bottom: (bCtx, dates) {
-            final d = dates.isEmpty
-                ? DateTime.now()
-                : DateTime.fromMillisecondsSinceEpoch(dates.first);
+          titleWidget: const Text('请选择日期'),
+          initialValue: _selected,
+          popupBottomExpanded: _expanded,
+          onCellClick: (value, selectType, tdate) => _expanded.value = true,
+          popupBottomBuilder: (bCtx, dates) {
+            final d = dates.isEmpty ? DateTime.now() : dates.first;
             return _WeatherPanel(date: d, weather: _weatherFor(d));
           },
           onConfirm: (value) => setState(() => _selected = value),
@@ -156,7 +145,7 @@ class _SingleCalendarCellState extends State<_SingleCalendarCell> {
 // ========================= 2. 多选 =========================
 /// 多选日历 + bottom 已选汇总
 ///
-/// 演示 [CalendarType.multiple] 多选模式,bottom 区域展示已选日期列表。
+/// 演示 [CalendarType.multiple] 多选模式,popupBottomBuilder 区域展示已选日期列表。
 class _MultipleCalendarCell extends StatefulWidget {
   const _MultipleCalendarCell();
   @override
@@ -164,7 +153,7 @@ class _MultipleCalendarCell extends StatefulWidget {
 }
 
 class _MultipleCalendarCellState extends State<_MultipleCalendarCell> {
-  List _dates = const [];
+  List _dates = const [];
 
   @override
   Widget build(BuildContext context) {
@@ -175,12 +164,10 @@ class _MultipleCalendarCellState extends State<_MultipleCalendarCell> {
       onClick: (_) {
         TCalendar.showPopup(
           context,
-          title: '请选择日期',
+          titleWidget: const Text('请选择日期'),
           type: CalendarType.multiple,
-          value: _dates.isEmpty
-              ? [DateTime.now().millisecondsSinceEpoch]
-              : _dates,
-          bottom: (bCtx, dates) => _MultipleSummary(selected: dates),
+          initialValue: _dates.isEmpty ? [DateTime.now()] : _dates,
+          popupBottomBuilder: (bCtx, dates) => _MultipleSummary(selected: dates),
           onConfirm: (value) => setState(() => _dates = value),
         );
       },
@@ -191,7 +178,7 @@ class _MultipleCalendarCellState extends State<_MultipleCalendarCell> {
 // ========================= 3. 区间 =========================
 /// 区间选择日历 + bottom 区间摘要
 ///
-/// 演示 [CalendarType.range] 区间模式,bottom 区域展示开始/结束日期及天数。
+/// 演示 [CalendarType.range] 区间模式,popupBottomBuilder 区域展示开始/结束日期及天数。
 class _RangeCalendarCell extends StatefulWidget {
   const _RangeCalendarCell();
   @override
@@ -199,9 +186,9 @@ class _RangeCalendarCell extends StatefulWidget {
 }
 
 class _RangeCalendarCellState extends State<_RangeCalendarCell> {
-  late List _dates = [
-    DateTime.now().millisecondsSinceEpoch,
-    DateTime.now().add(const Duration(days: 6)).millisecondsSinceEpoch,
+  late List _dates = [
+    DateTime.now(),
+    DateTime.now().add(const Duration(days: 6)),
   ];
 
   @override
@@ -215,10 +202,10 @@ class _RangeCalendarCellState extends State<_RangeCalendarCell> {
       onClick: (_) {
         TCalendar.showPopup(
           context,
-          title: '请选择日期区间',
+          titleWidget: const Text('请选择日期区间'),
           type: CalendarType.range,
-          value: _dates,
-          bottom: (bCtx, dates) => _RangeSummary(selected: dates),
+          initialValue: _dates,
+          popupBottomBuilder: (bCtx, dates) => _RangeSummary(selected: dates),
           onConfirm: (value) => setState(() => _dates = value),
         );
       },
@@ -229,8 +216,8 @@ class _RangeCalendarCellState extends State<_RangeCalendarCell> {
 // ========================= 4. 单选 + 时间 =========================
 /// 单选日历 + 时间选择器
 ///
-/// 演示 bottom 区域放置 [TPicker] 时间选择器,
-/// 确认时将日期和时间合并为完整时间戳。
+/// 演示 popupBottomBuilder 区域放置 [TPicker] 时间选择器,
+/// 确认时将日期和时间合并为完整 DateTime。
 class _SingleTimeCalendarCell extends StatefulWidget {
   const _SingleTimeCalendarCell();
   @override
@@ -239,7 +226,7 @@ class _SingleTimeCalendarCell extends StatefulWidget {
 }
 
 class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> {
-  late List _selected;
+  late List _selected;
   // 当前选中的时分(持久跨弹窗)
   late int _hour;
   late int _minute;
@@ -254,14 +241,13 @@ class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> {
     _selected = [
       DateTime.now()
           .add(const Duration(days: 30))
-          .copyWith(hour: _hour, minute: _minute, second: 0, millisecond: 0)
-          .millisecondsSinceEpoch,
+          .copyWith(hour: _hour, minute: _minute, second: 0, millisecond: 0),
     ];
   }
 
   @override
   Widget build(BuildContext context) {
-    final d = DateTime.fromMillisecondsSinceEpoch(_selected.first);
+    final d = _selected.first;
     return TCell(
       title: '单个选择日历和时间',
       arrow: true,
@@ -274,10 +260,10 @@ class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> {
         var popupMinute = _minute;
         TCalendar.showPopup(
           context,
-          title: '请选择日期和时间',
-          fixedHeight: 780,
-          value: _selected,
-          bottom: (_, __) => _TimePickerPanel(
+          titleWidget: const Text('请选择日期和时间'),
+          popupHeight: 780,
+          initialValue: _selected,
+          popupBottomBuilder: (_, __) => _TimePickerPanel(
             initialHour: popupHour,
             initialMinute: popupMinute,
             title: '选择时间',
@@ -287,15 +273,13 @@ class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> {
             },
           ),
           onConfirm: (dates) {
-            final merged = dates.map((ms) {
-              return DateTime.fromMillisecondsSinceEpoch(ms)
-                  .copyWith(
-                    hour: popupHour,
-                    minute: popupMinute,
-                    second: 0,
-                    millisecond: 0,
-                  )
-                  .millisecondsSinceEpoch;
+            final merged = dates.map((d) {
+              return d.copyWith(
+                hour: popupHour,
+                minute: popupMinute,
+                second: 0,
+                millisecond: 0,
+              );
             }).toList();
             setState(() {
               _selected = merged;
@@ -312,7 +296,7 @@ class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> {
 // ========================= 5. 区间 + 时间 =========================
 /// 区间选择 + 双时间选择器
 ///
-/// 演示 bottom 区域使用 [TTabBar] + [TPicker] 组合,
+/// 演示 popupBottomBuilder 区域使用 [TTabBar] + [TPicker] 组合,
 /// 点击日期单元格时自动切换开始/结束时间 Tab,
 /// 确认时分别合并开始和结束的日期+时间。
 class _RangeTimeCalendarCell extends StatefulWidget {
@@ -322,7 +306,7 @@ class _RangeTimeCalendarCell extends StatefulWidget {
 }
 
 class _RangeTimeCalendarCellState extends State<_RangeTimeCalendarCell> {
-  late List _dates;
+  late List _dates;
   // 持久跨弹窗的开始/结束时分
   late List _startTime;
   late List _endTime;
@@ -335,11 +319,10 @@ class _RangeTimeCalendarCellState extends State<_RangeTimeCalendarCell> {
     _endTime = [now.hour, now.minute];
     // 初始值就带上当前时分
     _dates = [
-      now.copyWith(second: 0, millisecond: 0).millisecondsSinceEpoch,
+      now.copyWith(second: 0, millisecond: 0),
       now
           .add(const Duration(days: 3))
-          .copyWith(second: 0, millisecond: 0)
-          .millisecondsSinceEpoch,
+          .copyWith(second: 0, millisecond: 0),
     ];
   }
 
@@ -357,18 +340,18 @@ class _RangeTimeCalendarCellState extends State<_RangeTimeCalendarCell> {
         final panelKey = GlobalKey<_RangeTimePickerPanelState>();
         TCalendar.showPopup(
           context,
-          title: '请选择日期和时间区间',
-          fixedHeight: 780,
+          titleWidget: const Text('请选择日期和时间区间'),
+          popupHeight: 780,
           type: CalendarType.range,
-          value: _dates,
-          onCellClick: (value, type, tdate) {
-            if (type == DateSelectType.start) {
+          initialValue: _dates,
+          onCellClick: (value, selectType, tdate) {
+            if (selectType == DateSelectType.start) {
               panelKey.currentState?.switchTab(0);
-            } else if (type == DateSelectType.end) {
+            } else if (selectType == DateSelectType.end) {
               panelKey.currentState?.switchTab(1);
             }
           },
-          bottom: (_, __) => _RangeTimePickerPanel(
+          popupBottomBuilder: (_, __) => _RangeTimePickerPanel(
             key: panelKey,
             initialStartTime: popupStartTime,
             initialEndTime: popupEndTime,
@@ -385,22 +368,18 @@ class _RangeTimeCalendarCellState extends State<_RangeTimeCalendarCell> {
               return;
             }
             final merged = [
-              DateTime.fromMillisecondsSinceEpoch(value[0])
-                  .copyWith(
-                    hour: popupStartTime[0],
-                    minute: popupStartTime[1],
-                    second: 0,
-                    millisecond: 0,
-                  )
-                  .millisecondsSinceEpoch,
-              DateTime.fromMillisecondsSinceEpoch(value[1])
-                  .copyWith(
-                    hour: popupEndTime[0],
-                    minute: popupEndTime[1],
-                    second: 0,
-                    millisecond: 0,
-                  )
-                  .millisecondsSinceEpoch,
+              value[0].copyWith(
+                hour: popupStartTime[0],
+                minute: popupStartTime[1],
+                second: 0,
+                millisecond: 0,
+              ),
+              value[1].copyWith(
+                hour: popupEndTime[0],
+                minute: popupEndTime[1],
+                second: 0,
+                millisecond: 0,
+              ),
             ];
             setState(() {
               _dates = merged;
@@ -426,7 +405,7 @@ class _AnchorCalendarCell extends StatefulWidget {
 }
 
 class _AnchorCalendarCellState extends State<_AnchorCalendarCell> {
-  List _selected = [DateTime(2026, 5, 1).millisecondsSinceEpoch];
+  List _selected = [DateTime(2026, 5, 1)];
 
   @override
   Widget build(BuildContext context) {
@@ -437,9 +416,9 @@ class _AnchorCalendarCellState extends State<_AnchorCalendarCell> {
       onClick: (_) {
         TCalendar.showPopup(
           context,
-          title: '请选择日期',
+          titleWidget: const Text('请选择日期'),
           anchorDate: DateTime(2026),
-          value: _selected,
+          initialValue: _selected,
           onConfirm: (dates) => setState(() => _selected = dates),
         );
       },
@@ -461,29 +440,26 @@ TPickerColumns _buildTimeItems() => TPickerColumns([
     ]);
 
 // ===== 顶层格式化辅助函数 =====
-String _formatYmd(List dates) {
+String _formatYmd(List dates) {
   if (dates.isEmpty) {
     return '--';
   }
-  final d = DateTime.fromMillisecondsSinceEpoch(dates.first);
+  final d = dates.first;
   return '${d.year}-${d.month.toString().padLeft(2, '0')}-'
       '${d.day.toString().padLeft(2, '0')}';
 }
 
-String _formatMd(int ms) {
-  final d = DateTime.fromMillisecondsSinceEpoch(ms);
+String _formatMd(DateTime d) {
   return '${d.month}/${d.day}';
 }
 
-String _formatMdHm(int ms) {
-  final d = DateTime.fromMillisecondsSinceEpoch(ms);
+String _formatMdHm(DateTime d) {
   return '${d.month}/${d.day} '
       '${d.hour.toString().padLeft(2, '0')}:'
       '${d.minute.toString().padLeft(2, '0')}';
 }
 
-String _formatYmdFull(int ms) {
-  final d = DateTime.fromMillisecondsSinceEpoch(ms);
+String _formatYmdFull(DateTime d) {
   return '${d.year}-${d.month.toString().padLeft(2, '0')}-'
       '${d.day.toString().padLeft(2, '0')}';
 }
@@ -619,7 +595,7 @@ class _IconRow extends StatelessWidget {
 class _MultipleSummary extends StatelessWidget {
   const _MultipleSummary({required this.selected});
 
-  final List selected;
+  final List selected;
 
   @override
   Widget build(BuildContext context) {
@@ -639,7 +615,7 @@ class _MultipleSummary extends StatelessWidget {
             spacing: 8,
             runSpacing: 6,
             children: dates
-                .map((ms) => Container(
+                .map((d) => Container(
                       padding: const EdgeInsets.symmetric(
                           horizontal: 8, vertical: 4),
                       decoration: BoxDecoration(
@@ -647,7 +623,7 @@ class _MultipleSummary extends StatelessWidget {
                         borderRadius: BorderRadius.circular(4),
                       ),
                       child: Text(
-                        _formatYmdFull(ms),
+                        _formatYmdFull(d),
                         style: TextStyle(
                             fontSize: 12,
                             color: TTheme.of(context).brandColor7),
@@ -664,14 +640,14 @@ class _MultipleSummary extends StatelessWidget {
 class _RangeSummary extends StatelessWidget {
   const _RangeSummary({required this.selected});
 
-  final List selected;
+  final List selected;
 
   @override
   Widget build(BuildContext context) {
     final hasStart = selected.isNotEmpty;
     final hasEnd = selected.length >= 2;
     final days = hasEnd
-        ? ((selected[1] - selected[0]) / (24 * 60 * 60 * 1000)).round() + 1
+        ? selected[1].difference(selected[0]).inDays + 1
         : (hasStart ? 1 : 0);
 
     return Container(
@@ -902,45 +878,62 @@ Widget _buildStyle(BuildContext context) {
     15: '元宵节',
   };
 
-  final customCellSelected = ValueNotifier>(
-      [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]);
+  final customCellSelected = ValueNotifier>(
+      [DateTime.now().add(const Duration(days: 30))]);
 
   return ValueListenableBuilder(
     valueListenable: customCellSelected,
     builder: (context, cellValue, _) {
-      final cellDate = DateTime.fromMillisecondsSinceEpoch(cellValue[0]);
+      final cellDate = cellValue[0];
       return TCellGroup(
         cells: [
-          // 1. 自定义文案(format 回调修改 prefix/suffix/style)
+          // 1. 自定义文案(cellWidget 回调自定义 cell 渲染)
           TCell(
             title: '自定义文案',
             arrow: true,
             onClick: (cell) {
               TCalendar.showPopup(
                 context,
-                title: '请选择日期',
-                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: TTheme.of(context).fontTitleMedium?.size,
-                        height: TTheme.of(context).fontTitleMedium?.height,
-                        fontWeight:
-                            TTheme.of(context).fontTitleMedium?.fontWeight,
-                        color: TTheme.of(context).errorColor6,
-                      );
-                      if (day?.typeNotifier.value == DateSelectType.selected) {
-                        day?.style = day.style
-                            ?.copyWith(color: TTheme.of(context).fontWhColor1);
-                      }
-                    }
-                  }
-                  return null;
+                titleWidget: const Text('请选择日期'),
+                minDate: DateTime(2022, 1, 1),
+                maxDate: DateTime(2022, 2, 15),
+                cellWidget: (context, tdate, selectType) {
+                  final isSpecial = tdate.date.month == 2 &&
+                      map.keys.contains(tdate.date.day);
+                  final suffix = isSpecial ? '¥100' : '¥60';
+                  final prefix = isSpecial ? map[tdate.date.day] : null;
+                  return Column(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      if (prefix != null)
+                        Text(prefix,
+                            style: TextStyle(
+                              fontSize: 9,
+                              color: isSpecial
+                                  ? TTheme.of(context).errorColor6
+                                  : null,
+                            )),
+                      Text(
+                        tdate.date.day.toString(),
+                        style: TextStyle(
+                          color: selectType == DateSelectType.selected
+                              ? TTheme.of(context).fontWhColor1
+                              : isSpecial
+                                  ? TTheme.of(context).errorColor6
+                                  : null,
+                        ),
+                      ),
+                      Text(suffix,
+                          style: TextStyle(
+                            fontSize: 9,
+                            color: selectType == DateSelectType.selected
+                                ? TTheme.of(context).fontWhColor1
+                                : isSpecial
+                                    ? TTheme.of(context).errorColor6
+                                    : null,
+                          )),
+                    ],
+                  );
                 },
               );
             },
@@ -953,8 +946,8 @@ Widget _buildStyle(BuildContext context) {
             onClick: (cell) {
               TCalendar.showPopup(
                 context,
-                title: '请选择日期',
-                value: [DateTime.now().millisecondsSinceEpoch],
+                titleWidget: const Text('请选择日期'),
+                initialValue: [DateTime.now()],
                 confirmBtn: Padding(
                   padding: EdgeInsets.symmetric(
                       vertical: TTheme.of(context).spacer16),
@@ -979,15 +972,14 @@ Widget _buildStyle(BuildContext context) {
             onClick: (cell) {
               TCalendar.showPopup(
                 context,
-                title: '请选择日期',
-                value: cellValue,
+                titleWidget: const Text('请选择日期'),
+                initialValue: cellValue,
                 cellHeight: 80,
                 onConfirm: (value) => customCellSelected.value = value,
                 cellWidget: (context, tdate, selectType) {
                   final today = DateTime.now();
-                  final isToday = tdate.date.millisecondsSinceEpoch ==
-                      DateTime(today.year, today.month, today.day)
-                          .millisecondsSinceEpoch;
+                  final isToday = tdate.date ==
+                      DateTime(today.year, today.month, today.day);
 
                   if (isToday && selectType != DateSelectType.selected) {
                     return _CustomCellContainer(
@@ -1036,357 +1028,363 @@ Widget _buildStyle(BuildContext context) {
   );
 }
 
-/// 「组件样式 - 不使用 Popup(内嵌模式)」
-///
-/// 直接将 [TCalendar] 嵌入页面布局,配合 [ValueListenableBuilder]
-/// 实现外部按钮控制选中日期的增减。
-@Demo(group: 'calendar')
-Widget _buildBlock(BuildContext context) {
-  final selected = ValueNotifier>(
-    [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000],
-  );
-  return Column(
-    crossAxisAlignment: CrossAxisAlignment.start,
-    children: [
-      Row(
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: [
-          TButton(
-            text: '加一个月',
-            theme: TButtonTheme.primary,
-            onTap: () {
-              selected.value = [selected.value[0] + 30 * 24 * 60 * 60 * 1000];
-            },
-          ),
-          const SizedBox(width: 16),
-          TButton(
-            text: '减一个月',
-            theme: TButtonTheme.primary,
-            onTap: () {
-              selected.value = [selected.value[0] - 30 * 24 * 60 * 60 * 1000];
-            },
-          ),
-        ],
-      ),
-      const SizedBox(height: 16),
-      ValueListenableBuilder(
-        valueListenable: selected,
-        builder: (context, value, child) {
-          return TCalendar(
-            title: '请选择日期',
-            value: value,
-            animateTo: true,
-          );
-        },
-      ),
-    ],
-  );
-}
-
 /// 「组件样式 - 农历日历」
 ///
-/// 演示内嵌模式下结合 [TCalendarDataSource] 展示农历信息,
-/// 支持月份切换、年份/月份跳转、农历信息开关。
+/// 非弹窗内嵌模式,结合 [TCalendarDataSource] 展示农历信息,
+/// 支持月份切换、年份/月份弹窗选择、农历信息开关。
 /// 点击日期时通过 SnackBar 显示完整的农历/节气/节日/假期信息。
 @Demo(group: 'calendar')
 Widget _buildLunar(BuildContext context) {
-  final dataSource = LunarDataSourceExample();
-  
-  // 当前月份状态
-  final currentMonth = ValueNotifier(
-    DateTime(DateTime.now().year, DateTime.now().month, 1),
-  );
-  
-  // 农历开关状态
-  final showLunarInfo = ValueNotifier(true);
-  
-  // 选中日期
-  final selectedDate = ValueNotifier>([
-    DateTime.now().millisecondsSinceEpoch,
-  ]);
-
-  return Column(
-    crossAxisAlignment: CrossAxisAlignment.start,
-    children: [
-      // 控制栏
-      Container(
-        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
-        decoration: BoxDecoration(
-          color: Colors.grey.shade50,
-          border: Border(
-            bottom: BorderSide(color: Colors.grey.shade200),
-          ),
+  return const _LunarCalendarDemo();
+}
+
+/// 农历日历内嵌演示
+///
+/// 控制栏(_LunarControlBar)与日历(TCalendar)分离:
+/// - 滑动日历时 onMonthChange 只更新控制栏,不重建日历,避免跳动
+/// - 点击控制栏导航时才更新 anchorDate,驱动日历滚动
+class _LunarCalendarDemo extends StatefulWidget {
+  const _LunarCalendarDemo();
+
+  @override
+  State<_LunarCalendarDemo> createState() => _LunarCalendarDemoState();
+}
+
+class _LunarCalendarDemoState extends State<_LunarCalendarDemo> {
+  final _dataSource = LunarDataSourceExample();
+
+  late DateTime _anchorDate;
+  late TCalendarDisplayMode _displayMode;
+  List _selected = [DateTime.now()];
+
+  @override
+  void initState() {
+    super.initState();
+    final now = DateTime.now();
+    _anchorDate = DateTime(now.year, now.month, 15);
+    _displayMode = TCalendarDisplayMode.solarWithLunar;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        // ---- 控制栏(独立 Widget,onMonthChange 只更新它自己) ----
+        _LunarControlBar(
+          key: _LunarControlBar.monthKey,
+          dataSource: _dataSource,
+          onNavigate: (anchor) {
+            setState(() => _anchorDate = anchor);
+          },
+          onModeChanged: (mode) {
+            setState(() => _displayMode = mode);
+          },
         ),
-        child: ValueListenableBuilder(
-          valueListenable: currentMonth,
-          builder: (context, month, child) {
-            // 获取当前月份的农历信息
-            final lunarInfo = dataSource.getLunarInfo(month);
-            final lunarMonth = lunarInfo != null 
-                ? '${lunarInfo.yearText}年 ${lunarInfo.monthText}' 
-                : '';
-
-            return Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                // 农历年月显示
-                if (lunarMonth.isNotEmpty)
-                  Padding(
-                    padding: const EdgeInsets.only(bottom: 8),
-                    child: Text(
-                      lunarMonth,
-                      style: TextStyle(
-                        fontSize: 14,
-                        color: Colors.grey.shade700,
-                        fontWeight: FontWeight.w500,
-                      ),
-                    ),
+
+        // ---- 内嵌日历 ----
+        TCalendar(
+          type: CalendarType.single,
+          minDate: DateTime(2020, 1, 1),
+          maxDate: DateTime(2030, 12, 31),
+          initialValue: _selected,
+          anchorDate: _anchorDate,
+          animateTo: true,
+          displayMode: _displayMode,
+          dataSource: _dataSource,
+          onMonthChange: (month) {
+            // 只通知控制栏更新显示,不 setState 本身 → 日历不重建
+            _LunarControlBar.monthKey.currentState
+                ?.updateMonth(DateTime(month.year, month.month, 1));
+          },
+          onCellClick: (date, selectType, tdate) {
+            final lunarInfo = _dataSource.getLunarInfo(date);
+            final solarTerm = _dataSource.getSolarTerm(date);
+            final festival = _dataSource.getFestival(date, lunarInfo);
+            final holidayInfo = _dataSource.getHolidayInfo(date);
+
+            final buffer = StringBuffer();
+            buffer.write('阳历:${date.year}年${date.month}月${date.day}日');
+
+            if (lunarInfo != null) {
+              buffer.write('\n农历:${lunarInfo.monthText}${lunarInfo.dayText}');
+            }
+
+            if (solarTerm != null && solarTerm.isNotEmpty) {
+              buffer.write('\n节气:$solarTerm');
+            }
+
+            if (festival != null && festival.isNotEmpty) {
+              buffer.write('\n节日:$festival');
+            }
+
+            if (holidayInfo != null) {
+              final type = holidayInfo['type'] == 'holiday' ? '假期' : '调休';
+              buffer.write('\n$type:${holidayInfo['name']}');
+            }
+
+            ScaffoldMessenger.of(context).clearSnackBars();
+            ScaffoldMessenger.of(context).showSnackBar(
+              SnackBar(
+                content: Text(buffer.toString()),
+                duration: const Duration(seconds: 3),
+                behavior: SnackBarBehavior.floating,
+              ),
+            );
+          },
+          onChange: (value) {
+            setState(() => _selected = value);
+          },
+        ),
+      ],
+    );
+  }
+}
+
+/// 农历日历控制栏
+///
+/// 独立管理 _currentMonth 状态,滑动日历时只更新本 Widget,
+/// 不触发上层日历重建,避免跳动。
+class _LunarControlBar extends StatefulWidget {
+  const _LunarControlBar({
+    super.key,
+    required this.dataSource,
+    required this.onNavigate,
+    required this.onModeChanged,
+  });
+
+  final LunarDataSourceExample dataSource;
+  final ValueChanged onNavigate;
+  final ValueChanged onModeChanged;
+
+  /// 全局 Key,供父 Widget 通过 currentState.updateMonth() 同步月份
+  static final GlobalKey<_LunarControlBarState> monthKey =
+      GlobalKey<_LunarControlBarState>();
+
+  @override
+  State<_LunarControlBar> createState() => _LunarControlBarState();
+}
+
+class _LunarControlBarState extends State<_LunarControlBar> {
+  late DateTime _currentMonth;
+  late TCalendarDisplayMode _mode;
+
+  // 使用 ValueNotifier 避免滚动时 setState 导致整个控制栏重建和布局重算
+  final _yearTextNotifier = ValueNotifier('');
+  final _monthTextNotifier = ValueNotifier('');
+  final _lunarTextNotifier = ValueNotifier('');
+
+  @override
+  void initState() {
+    super.initState();
+    final now = DateTime.now();
+    _currentMonth = DateTime(now.year, now.month, 1);
+    _mode = TCalendarDisplayMode.solarWithLunar;
+    _updateNotifiers();
+  }
+
+  @override
+  void dispose() {
+    _yearTextNotifier.dispose();
+    _monthTextNotifier.dispose();
+    _lunarTextNotifier.dispose();
+    super.dispose();
+  }
+
+  void _updateNotifiers() {
+    _yearTextNotifier.value = '${_currentMonth.year}年';
+    _monthTextNotifier.value = '${_currentMonth.month}月';
+    final lunarInfo = widget.dataSource.getLunarInfo(_currentMonth);
+    _lunarTextNotifier.value = lunarInfo != null
+        ? '${lunarInfo.yearText}年 ${lunarInfo.monthText}'
+        : '';
+  }
+
+  /// 由日历 onMonthChange 回调驱动,仅更新 ValueNotifier,不触发 setState
+  void updateMonth(DateTime month) {
+    if (_currentMonth.year == month.year && _currentMonth.month == month.month) {
+      return;
+    }
+    _currentMonth = month;
+    _updateNotifiers();
+  }
+
+  void _navigateTo(DateTime month) {
+    _currentMonth = month;
+    _updateNotifiers();
+    widget.onNavigate(DateTime(month.year, month.month, 15));
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          // 固定高度容器,防止农历文字有无时布局跳动
+          SizedBox(
+            height: 20,
+            child: ValueListenableBuilder(
+              valueListenable: _lunarTextNotifier,
+              builder: (context, lunarMonth, _) {
+                if (lunarMonth.isEmpty) return const SizedBox.shrink();
+                return Padding(
+                  padding: const EdgeInsets.only(bottom: 4),
+                  child: Text(
+                    lunarMonth,
+                    style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
                   ),
-                // 按钮行
-                Row(
-                  children: [
-                    // 上一月按钮
-                    TButton(
-                      text: '上一月',
-                      size: TButtonSize.small,
-                      theme: TButtonTheme.primary,
-                      onTap: () {
-                        currentMonth.value = DateTime(
-                          month.year,
-                          month.month - 1,
-                          1,
-                        );
-                        selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                      },
-                    ),
-                    const SizedBox(width: 8),
-                    // 年份选择
-                    Expanded(
-                      child: TButton(
-                        text: '${month.year}年',
-                        size: TButtonSize.small,
-                        theme: TButtonTheme.defaultTheme,
-                        onTap: () async {
-                          final year = await showModalBottomSheet(
-                            context: context,
-                            builder: (context) {
-                              return SizedBox(
-                                height: 300,
-                                child: Column(
-                                  children: [
-                                    const Padding(
-                                      padding: EdgeInsets.all(16),
-                                      child: Text(
-                                        '选择年份',
-                                        style: TextStyle(
-                                          fontSize: 18,
-                                          fontWeight: FontWeight.bold,
-                                        ),
-                                      ),
-                                    ),
-                                    Expanded(
-                                      child: ListView.builder(
-                                        itemCount: 50,
-                                        itemBuilder: (context, index) {
-                                          final year = DateTime.now().year - 10 + index;
-                                          final isSelected = year == month.year;
-                                          return ListTile(
-                                            title: Text(
-                                              '$year年',
-                                              style: TextStyle(
-                                                color: isSelected ? Colors.blue : null,
-                                                fontWeight: isSelected ? FontWeight.bold : null,
-                                              ),
-                                            ),
-                                            onTap: () => Navigator.pop(context, year),
-                                          );
-                                        },
-                                      ),
-                                    ),
-                                  ],
+                );
+              },
+            ),
+          ),
+          Row(
+            children: [
+              TButton(
+                text: '◀',
+                size: TButtonSize.small,
+                theme: TButtonTheme.defaultTheme,
+                onTap: () => _navigateTo(DateTime(_currentMonth.year, _currentMonth.month - 1, 1)),
+              ),
+              const SizedBox(width: 4),
+              Expanded(
+                child: ValueListenableBuilder(
+                  valueListenable: _yearTextNotifier,
+                  builder: (context, text, _) => TButton(
+                    text: text,
+                    size: TButtonSize.small,
+                    theme: TButtonTheme.defaultTheme,
+                    onTap: () async {
+                      final year = await showModalBottomSheet(
+                        context: context,
+                        builder: (context) {
+                          return SizedBox(
+                            height: 300,
+                            child: Column(
+                              children: [
+                                const Padding(
+                                  padding: EdgeInsets.all(16),
+                                  child: Text('选择年份',
+                                      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                                 ),
-                              );
-                            },
+                                Expanded(
+                                  child: ListView.builder(
+                                    itemCount: 50,
+                                    itemBuilder: (context, index) {
+                                      final y = DateTime.now().year - 10 + index;
+                                      return ListTile(
+                                        title: Text('$y年',
+                                            style: TextStyle(
+                                              color: y == _currentMonth.year ? Colors.blue : null,
+                                              fontWeight: y == _currentMonth.year ? FontWeight.bold : null,
+                                            )),
+                                        onTap: () => Navigator.pop(context, y),
+                                      );
+                                    },
+                                  ),
+                                ),
+                              ],
+                            ),
                           );
-                          if (year != null) {
-                            currentMonth.value = DateTime(year, month.month, 1);
-                            selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                          }
                         },
-                      ),
-                    ),
-                    const SizedBox(width: 8),
-                    // 月份选择
-                    Expanded(
-                      child: TButton(
-                        text: '${month.month}月',
-                        size: TButtonSize.small,
-                        theme: TButtonTheme.defaultTheme,
-                        onTap: () async {
-                          final selectedMonth = await showModalBottomSheet(
-                            context: context,
-                            builder: (context) {
-                              return SizedBox(
-                                height: 400,
-                                child: Column(
-                                  children: [
-                                    const Padding(
-                                      padding: EdgeInsets.all(16),
-                                      child: Text(
-                                        '选择月份',
-                                        style: TextStyle(
-                                          fontSize: 18,
-                                          fontWeight: FontWeight.bold,
-                                        ),
-                                      ),
+                      );
+                      if (year != null) {
+                        _navigateTo(DateTime(year, _currentMonth.month, 1));
+                      }
+                    },
+                  ),
+                ),
+              ),
+              const SizedBox(width: 4),
+              Expanded(
+                child: ValueListenableBuilder(
+                  valueListenable: _monthTextNotifier,
+                  builder: (context, text, _) => TButton(
+                    text: text,
+                    size: TButtonSize.small,
+                    theme: TButtonTheme.defaultTheme,
+                    onTap: () async {
+                      final m = await showModalBottomSheet(
+                        context: context,
+                        builder: (context) {
+                          return SizedBox(
+                            height: 400,
+                            child: Column(
+                              children: [
+                                const Padding(
+                                  padding: EdgeInsets.all(16),
+                                  child: Text('选择月份',
+                                      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
+                                ),
+                                Expanded(
+                                  child: GridView.builder(
+                                    padding: const EdgeInsets.all(16),
+                                    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+                                      crossAxisCount: 3,
+                                      childAspectRatio: 2,
+                                      crossAxisSpacing: 10,
+                                      mainAxisSpacing: 10,
                                     ),
-                                    Expanded(
-                                      child: GridView.builder(
-                                        padding: const EdgeInsets.all(16),
-                                        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
-                                          crossAxisCount: 3,
-                                          childAspectRatio: 2,
-                                          crossAxisSpacing: 10,
-                                          mainAxisSpacing: 10,
+                                    itemCount: 12,
+                                    itemBuilder: (context, index) {
+                                      final m = index + 1;
+                                      final isSelected = m == _currentMonth.month;
+                                      return InkWell(
+                                        onTap: () => Navigator.pop(context, m),
+                                        child: Container(
+                                          alignment: Alignment.center,
+                                          decoration: BoxDecoration(
+                                            color: isSelected ? Colors.blue : Colors.grey.shade200,
+                                            borderRadius: BorderRadius.circular(8),
+                                          ),
+                                          child: Text('$m月',
+                                              style: TextStyle(
+                                                color: isSelected ? Colors.white : Colors.black,
+                                                fontWeight: isSelected ? FontWeight.bold : null,
+                                              )),
                                         ),
-                                        itemCount: 12,
-                                        itemBuilder: (context, index) {
-                                          final m = index + 1;
-                                          final isSelected = m == month.month;
-                                          return InkWell(
-                                            onTap: () => Navigator.pop(context, m),
-                                            child: Container(
-                                              alignment: Alignment.center,
-                                              decoration: BoxDecoration(
-                                                color: isSelected ? Colors.blue : Colors.grey.shade200,
-                                                borderRadius: BorderRadius.circular(8),
-                                              ),
-                                              child: Text(
-                                                '$m月',
-                                                style: TextStyle(
-                                                  color: isSelected ? Colors.white : Colors.black,
-                                                  fontWeight: isSelected ? FontWeight.bold : null,
-                                                ),
-                                              ),
-                                            ),
-                                          );
-                                        },
-                                      ),
-                                    ),
-                                  ],
+                                      );
+                                    },
+                                  ),
                                 ),
-                              );
-                            },
+                              ],
+                            ),
                           );
-                          if (selectedMonth != null) {
-                            currentMonth.value = DateTime(month.year, selectedMonth, 1);
-                            selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                          }
                         },
-                      ),
-                    ),
-                    const SizedBox(width: 8),
-                    // 下一月按钮
-                    TButton(
-                      text: '下一月',
-                      size: TButtonSize.small,
-                      theme: TButtonTheme.primary,
-                      onTap: () {
-                        currentMonth.value = DateTime(
-                          month.year,
-                          month.month + 1,
-                          1,
-                        );
-                        selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                      },
-                    ),
-                    const SizedBox(width: 16),
-                    // 农历开关
-                    ValueListenableBuilder(
-                      valueListenable: showLunarInfo,
-                      builder: (context, show, child) {
-                        return Row(
-                          mainAxisSize: MainAxisSize.min,
-                          children: [
-                            Text(
-                              '农历',
-                              style: TextStyle(fontSize: 12, color: Colors.grey.shade700),
-                            ),
-                            Switch(
-                              value: show,
-                              onChanged: (value) {
-                                showLunarInfo.value = value;
-                              },
-                              materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
-                            ),
-                          ],
-                        );
-                      },
-                    ),
-                  ],
+                      );
+                      if (m != null) {
+                        _navigateTo(DateTime(_currentMonth.year, m, 1));
+                      }
+                    },
+                  ),
                 ),
-              ],
-            );
-          },
-        ),
-      ),
-      const SizedBox(height: 16),
-      // 日历主体
-      ValueListenableBuilder(
-        valueListenable: showLunarInfo,
-        builder: (context, show, child) {
-          return ValueListenableBuilder(
-            valueListenable: selectedDate,
-            builder: (context, value, child) {
-              return TCalendar(
-                title: '',
-                showLunarInfo: show,
-                dataSource: dataSource,
-                value: value,
-                onChange: (newValue) {
-                  selectedDate.value = newValue;
-                  
-                  // 显示完整农历信息
-                  final date = DateTime.fromMillisecondsSinceEpoch(newValue[0]);
-                  final lunarInfo = dataSource.getLunarInfo(date);
-                  final solarTerm = dataSource.getSolarTerm(date);
-                  final festival = dataSource.getFestival(date, lunarInfo);
-                  final holidayInfo = dataSource.getHolidayInfo(date);
-                  
-                  final buffer = StringBuffer();
-                  buffer.write('阳历:${date.year}年${date.month}月${date.day}日');
-                  
-                  if (lunarInfo != null) {
-                    buffer.write('\n农历:${lunarInfo.monthText}${lunarInfo.dayText}');
-                  }
-                  
-                  if (solarTerm != null && solarTerm.isNotEmpty) {
-                    buffer.write('\n节气:$solarTerm');
-                  }
-                  
-                  if (festival != null && festival.isNotEmpty) {
-                    buffer.write('\n节日:$festival');
-                  }
-                  
-                  if (holidayInfo != null) {
-                    final type = holidayInfo['type'] == 'holiday' ? '假期' : '调休';
-                    buffer.write('\n$type:${holidayInfo['name']}');
-                  }
-                  
-                  ScaffoldMessenger.of(context).clearSnackBars();
-                  ScaffoldMessenger.of(context).showSnackBar(
-                    SnackBar(
-                      content: Text(buffer.toString()),
-                      duration: const Duration(seconds: 3),
-                      behavior: SnackBarBehavior.floating,
-                    ),
-                  );
+              ),
+              const SizedBox(width: 4),
+              TButton(
+                text: '▶',
+                size: TButtonSize.small,
+                theme: TButtonTheme.defaultTheme,
+                onTap: () => _navigateTo(DateTime(_currentMonth.year, _currentMonth.month + 1, 1)),
+              ),
+              const SizedBox(width: 8),
+              Text('农历', style: TextStyle(fontSize: 12, color: Colors.grey.shade700)),
+              Switch(
+                value: _mode == TCalendarDisplayMode.solarWithLunar ||
+                    _mode == TCalendarDisplayMode.lunar,
+                onChanged: (v) {
+                  final newMode = v
+                      ? TCalendarDisplayMode.solarWithLunar
+                      : TCalendarDisplayMode.solar;
+                  setState(() => _mode = newMode);
+                  widget.onModeChanged(newMode);
                 },
-              );
-            },
-          );
-        },
+                materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+              ),
+            ],
+          ),
+        ],
       ),
-    ],
-  );
+    );
+  }
 }
diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart
index c42ee56f2..2a2205cc4 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart
@@ -3,11 +3,12 @@ import 'package:flutter/material.dart';
 import '../../../tdesign_flutter.dart';
 import '../../util/context_extension.dart';
 import '../../util/iterable_ext.dart';
+import 't_calendar_body.dart';
+import 't_calendar_cell.dart';
+import 't_calendar_header.dart';
 
-export 't_calendar_body.dart';
-export 't_calendar_cell.dart';
+export 't_calendar_cell.dart' show TDate;
 export 't_calendar_data_source.dart';
-export 't_calendar_header.dart';
 export 't_calendar_style.dart';
 export 't_lunar_date.dart';
 
@@ -37,10 +38,10 @@ class TCalendarInherited extends InheritedWidget {
   /// 选中态的可写引用(仅供 [TCalendar] 内部更新使用)。
   ///
   /// 对外消费方请使用 [selectedListenable] 这一只读视图。
-  final ValueNotifier> selected;
+  final ValueNotifier> selected;
 
   /// 选中态的只读视图,供下游 widget 监听变化。
-  ValueListenable> get selectedListenable => selected;
+  ValueListenable> get selectedListenable => selected;
 
   final bool? usePopup;
 
@@ -69,133 +70,122 @@ class TCalendarInherited extends InheritedWidget {
   }
 }
 
-typedef CalendarFormat = TDate? Function(TDate? day);
+/// 日历选择模式
+enum CalendarType {
+  /// 单选:点击新日期时自动取消旧日期的选中状态
+  single,
 
-/// [TCalendar.bottom] 的构建器签名。`selectedDates` 为只读视图,详细行为见 [TCalendar.bottom]。
-typedef CalendarBottomBuilder = Widget Function(
-  BuildContext context,
-  List selectedDates,
-);
+  /// 多选:点击日期切换选中/取消,可同时选中多个日期
+  multiple,
 
-enum CalendarType { single, multiple, range }
+  /// 区间选择:第一次点击选起点,第二次点击选终点,中间自动填充
+  range,
+}
 
+/// 日期在日历中的选中状态
 enum DateSelectType { selected, disabled, start, centre, end, empty }
 
+/// 日历显示模式,控制日期单元格的主/副文本内容
+enum TCalendarDisplayMode {
+  /// 纯阳历:主文本显示阳历日期数字,无副文本
+  solar,
+
+  /// 阳历 + 农历副标题:主文本显示阳历日期数字,副文本显示农历(如"初七")
+  solarWithLunar,
+
+  /// 农历为主:主文本显示农历(如"初七"),副文本显示阳历日期数字
+  lunar,
+}
+
 /// 日历组件
 class TCalendar extends StatefulWidget {
   const TCalendar({
     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.initialValue,
+    this.cellHeight,
     this.height,
-    this.width,
     this.style,
     this.onChange,
     this.onCellClick,
-    this.onCellLongPress,
-    this.onHeaderClick,
-    this.useSafeArea = true,
-    this.bottom,
-    this.bottomExpanded,
+    this.safeAreaInset = true,
+    this.popupBottomBuilder,
+    this.popupBottomExpanded,
     this.monthTitleHeight = 22,
     this.monthTitleBuilder,
     this.animateTo = false,
     this.cellWidget,
     this.onMonthChange,
     this.anchorDate,
-    this.dateType = TCalendarDateType.solar,
+    this.displayMode = TCalendarDisplayMode.solar,
     this.dataSource,
-    this.showLunarInfo = false,
   })  : assert(
-          bottomExpanded == null || bottom != null,
-          'bottomExpanded 需配合 bottom 使用',
+          popupBottomExpanded == null || popupBottomBuilder != null,
+          'popupBottomExpanded 需配合 popupBottomBuilder 使用',
         ),
         super(key: key);
 
-  /// 第一天从星期几开始,默认 0 = 周日
-  final int? firstDayOfWeek;
+  /// 第一天从星期几开始,0 = 周日,1 = 周一,…,6 = 周六。默认 0(周日)。
+  final int firstDayOfWeek;
 
-  /// 用于格式化日期的函数,可定义日期前后的显示内容和日期样式
-  final CalendarFormat? format;
+  /// 最大可选的日期,不传则默认 2100-12-31
+  final DateTime? maxDate;
 
-  /// 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认 2100-12-31
-  final int? maxDate;
+  /// 最小可选的日期,不传则默认 1970-01-01
+  final DateTime? minDate;
 
-  /// 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认 1970-01-01
-  final int? minDate;
-
-  /// 标题
-  final String? title;
-
-  /// 标题组件
+  /// 标题组件,可传入 Text 或自定义 Widget
   final Widget? titleWidget;
 
-  /// 日历的选择类型,single = 单选;multiple = 多选;range = 区间选择
-  final CalendarType? type;
-
-  /// 当前选择的日期(fromMillisecondsSinceEpoch),不传则默认今天,当 type = single 时数组长度为1
-  final List? value;
+  /// 日历的选择模式,决定点击日期后的选中行为:
+  /// - [CalendarType.single]:单选,点击新日期取消旧选中
+  /// - [CalendarType.multiple]:多选,点击切换选中/取消
+  /// - [CalendarType.range]:区间选择,依次选起止日期
+  final CalendarType type;
 
-  /// 年月显示格式,`year`表示年,`month`表示月,如`year month`表示年在前、月在后、中间隔一个空格
-  final String? displayFormat;
+  /// 初始选中日期列表,不传则默认今天。
+  ///
+  /// 列表长度与 [type] 对应:
+  /// - [CalendarType.single]:1 个元素(选中日期)
+  /// - [CalendarType.multiple]:N 个元素(所有选中日期)
+  /// - [CalendarType.range]:2 个元素(起始、结束日期)
+  final List? initialValue;
 
   /// 高度,不传时内嵌模式自动按 5 行日期计算
   final double? height;
 
-  /// 日期高度
+  /// 日期单元格高度,默认 60。如需更大行高可传入自定义值(如 80)
   final double? cellHeight;
 
-  /// 宽度
-  final double? width;
-
-  /// 锚点日期
-  final DateTime? anchorDate;
-
   /// 自定义样式
   final TCalendarStyle? style;
 
   /// 选中值变化时触发
-  final void Function(List value)? onChange;
+  final void Function(List value)? onChange;
 
   /// 点击日期时触发
   final void Function(
-    int value,
-    DateSelectType type,
+    DateTime value,
+    DateSelectType selectType,
     TDate tdate,
   )? onCellClick;
 
-  /// 长按日期时触发
-  final void Function(
-    int value,
-    DateSelectType type,
-    TDate tdate,
-  )? onCellLongPress;
-
-  /// 点击周时触发
-  final void Function(
-    int index,
-    String week,
-  )? onHeaderClick;
-
   /// 月份变化时触发
   final ValueChanged? onMonthChange;
 
-  /// 是否使用安全区域(默认 true)
-  final bool? useSafeArea;
+  /// 是否适配底部安全区域(如 iPhone Home Indicator),默认 true
+  final bool safeAreaInset;
 
-  /// 底部自定义区域构建器,以浮层方式叠加在日历主体之上。
+  /// 弹窗底部自定义区域构建器,以浮层方式叠加在日历主体之上。
   ///
-  /// **仅能在 `TPopupBottomDisplayPanel` 内使用**(作为其 `child` 的子树)。
+  /// **仅在弹窗模式下生效**(即 [TCalendar] 作为 [TPopupBottomDisplayPanel] 的子树时)。
+  /// 非弹窗模式下传入此参数将被忽略。
   ///
-  /// - **不会撑高 [TCalendar]**,请在面板 `fixedHeight` 中预留 bottom 自身的占用高度;
+  /// - **不会撑高 [TCalendar]**,请在 `popupHeight` 中预留 bottom 自身的占用高度;
   /// - `selectedDates` 随弹窗内日期点击实时更新;
   /// - 传入的 `selectedDates` 是只读视图([List.unmodifiable]),请勿原地修改。
   ///
@@ -203,13 +193,14 @@ class TCalendar extends StatefulWidget {
   /// TPopupBottomDisplayPanel(
   ///   fixedHeight: 600,
   ///   child: TCalendar(
-  ///     bottom: (ctx, dates) => MyFooter(selectedDates: dates),
+  ///     popupBottomBuilder: (ctx, dates) => MyFooter(selectedDates: dates),
   ///   ),
   /// );
   /// ```
-  final CalendarBottomBuilder? bottom;
+  final Widget Function(BuildContext context, List selectedDates)?
+      popupBottomBuilder;
 
-  /// bottom 区域是否展开(响应式)。**仅能在 [TPopupBottomDisplayPanel] 内使用。**
+  /// 弹窗底部区域是否展开(响应式)。**仅在弹窗模式下生效。**
   ///
   /// 为 `null`(默认)时 bottom 始终展开;传入 [ValueListenable] 时,
   /// bottom 展开/收起将跟随其值变化播放滑动动画,常配合 [ValueNotifier] 使用。
@@ -219,16 +210,16 @@ class TCalendar extends StatefulWidget {
   /// TPopupBottomDisplayPanel(
   ///   fixedHeight: 600,
   ///   child: TCalendar(
-  ///     bottomExpanded: expanded,
+  ///     popupBottomExpanded: expanded,
   ///     onCellClick: (v, t, d) => expanded.value = true,
-  ///     bottom: (ctx, dates) => MyFooter(),
+  ///     popupBottomBuilder: (ctx, dates) => MyFooter(),
   ///   ),
   /// );
   /// ```
-  final ValueListenable? bottomExpanded;
+  final ValueListenable? popupBottomExpanded;
 
-  /// 月标题高度
-  final double? monthTitleHeight;
+  /// 每月标题行高度(如 '2025年6月' 所在行),默认 22
+  final double monthTitleHeight;
 
   /// 月标题构建器
   final Widget Function(
@@ -236,8 +227,8 @@ class TCalendar extends StatefulWidget {
     DateTime monthDate,
   )? monthTitleBuilder;
 
-  /// 动画滚动到指定位置
-  final bool? animateTo;
+  /// 滚动到选中日期/锚点日期所在月份时是否使用动画,默认 false
+  final bool animateTo;
 
   /// 自定义日期单元格组件
   final Widget? Function(
@@ -246,18 +237,23 @@ class TCalendar extends StatefulWidget {
     DateSelectType selectType,
   )? cellWidget;
 
-  /// 日历类型:阳历或农历
-  final TCalendarDateType dateType;
+  /// 锚点日期,弹出时自动滚动到该日期所在月份。
+  /// 传入 [DateTime] 对象,如 `DateTime(2025, 6, 15)`。
+  final DateTime? anchorDate;
 
-  /// 外部数据源,用于提供农历转换等功能
-  final TCalendarDataSource? dataSource;
+  /// 日历显示模式,控制日期单元格的主/副文本内容:
+  /// - [TCalendarDisplayMode.solar]:纯阳历,主文本显示阳历日期数字
+  /// - [TCalendarDisplayMode.solarWithLunar]:阳历 + 农历副标题
+  /// - [TCalendarDisplayMode.lunar]:农历为主文本,阳历为副文本
+  final TCalendarDisplayMode displayMode;
 
-  /// 阳历模式下是否显示农历信息作为副标题
-  final bool showLunarInfo;
+  /// 外部数据源,用于提供农历转换等功能。
+  /// 当 [displayMode] 为 [TCalendarDisplayMode.solarWithLunar] 或
+  /// [TCalendarDisplayMode.lunar] 时必须提供。
+  final TCalendarDataSource? dataSource;
 
-  List? get _value => value?.map((e) {
-        final date = DateTime.fromMillisecondsSinceEpoch(e);
-        return DateTime(date.year, date.month, date.day);
+  List? get _value => initialValue?.map((e) {
+        return DateTime(e.year, e.month, e.day);
       }).toList();
 
   // ---------------------------------------------------------------------------
@@ -287,12 +283,12 @@ class TCalendar extends StatefulWidget {
 
   /// 弹出日历选择器,返回选中的日期列表。
   ///
-  /// 取消或关闭弹窗时返回 `null`;点击确认时返回选中日期的毫秒时间戳列表。
+  /// 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。
   ///
   /// ```dart
   /// final result = await TCalendar.showPopup(
   ///   context,
-  ///   title: '请选择日期',
+  ///   titleWidget: Text('请选择日期'),
   ///   type: CalendarType.single,
   /// );
   /// if (result != null) {
@@ -302,64 +298,57 @@ class TCalendar extends StatefulWidget {
   ///
   /// 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel]
   /// + [TSlidePopupRoute] 自行组装。
-  static Future?> showPopup(
+  static Future?> showPopup(
     BuildContext context, {
-    /// 弹窗标题
-    String? title,
+    /// 弹窗标题组件
+    Widget? titleWidget,
 
-    /// 日历选择类型
+    /// 日历选择模式
     CalendarType type = CalendarType.single,
 
-    /// 当前选中的日期(毫秒时间戳列表)
-    List? value,
+    /// 初始选中日期列表
+    List? initialValue,
 
-    /// 最小可选日期(毫秒时间戳)
-    int? minDate,
+    /// 最小可选日期
+    DateTime? minDate,
 
-    /// 最大可选日期(毫秒时间戳)
-    int? maxDate,
+    /// 最大可选日期
+    DateTime? maxDate,
 
-    /// 锚点日期,弹出时滚动到该月
+    /// 锚点日期,弹出时自动滚动到该日期所在月份
     DateTime? anchorDate,
 
-    /// 面板固定高度(不传时自动计算)
-    double? fixedHeight,
-
-    /// 第一天从星期几开始,默认 0 = 周日
-    int? firstDayOfWeek,
+    /// 弹窗面板高度(不传时自动计算)
+    double? popupHeight,
 
-    /// 年月显示格式
-    String? displayFormat,
+    /// 第一天从星期几开始,0 = 周日,1 = 周一,…,6 = 周六。默认 0(周日)。
+    int firstDayOfWeek = 0,
 
-    /// 日期高度
+    /// 日期单元格高度
     double? cellHeight,
 
     /// 自定义样式
     TCalendarStyle? style,
 
-    /// 用于格式化日期的函数
-    CalendarFormat? format,
+    /// 弹窗底部自定义区域构建器
+    Widget Function(BuildContext context, List selectedDates)?
+        popupBottomBuilder,
 
-    /// 底部自定义区域构建器
-    CalendarBottomBuilder? bottom,
-
-    /// bottom 区域是否展开(响应式)
-    ValueListenable? bottomExpanded,
+    /// 弹窗底部区域是否展开(响应式)
+    ValueListenable? popupBottomExpanded,
 
     /// 自定义确认按钮
     Widget? confirmBtn,
 
-    /// 点击确认的额外回调(除了返回值之外)
-    void Function(List)? onConfirm,
+    /// 点击确认按钮时触发
+    void Function(List)? onConfirm,
 
-    /// 弹窗关闭后的回调
+    /// 弹窗关闭后触发(无论确认还是取消)
     VoidCallback? onClose,
 
     /// 点击日期时触发
-    void Function(int value, DateSelectType type, TDate tdate)? onCellClick,
-
-    /// 长按日期时触发
-    void Function(int value, DateSelectType type, TDate tdate)? onCellLongPress,
+    void Function(DateTime value, DateSelectType selectType, TDate tdate)?
+        onCellClick,
 
     /// 点击遮罩或物理返回是否关闭
     bool autoClose = true,
@@ -374,23 +363,20 @@ class TCalendar extends StatefulWidget {
       DateSelectType selectType,
     )? cellWidget,
 
-    /// 日历类型:阳历或农历
-    TCalendarDateType dateType = TCalendarDateType.solar,
+    /// 日历显示模式
+    TCalendarDisplayMode displayMode = TCalendarDisplayMode.solar,
 
     /// 外部数据源,用于提供农历转换等功能
     TCalendarDataSource? dataSource,
 
-    /// 阳历模式下是否显示农历信息作为副标题
-    bool showLunarInfo = false,
-
     /// 月份变化时触发
     ValueChanged? onMonthChange,
 
     /// 月标题构建器
     Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder,
   }) async {
-    final selected = ValueNotifier>(value ?? []);
-    List? result;
+    final selected = ValueNotifier>(initialValue ?? []);
+    List? result;
     var closing = false;
 
     void doClose(NavigatorState nav) {
@@ -410,7 +396,13 @@ class TCalendar extends StatefulWidget {
         final screenHeight = MediaQuery.of(ctx).size.height;
 
         final panelHeight =
-            fixedHeight ?? _calcDefaultHeight(safeBottom, screenHeight);
+            popupHeight ?? _calcDefaultHeight(safeBottom, screenHeight);
+
+        // 提取标题文字给 TPopupBottomDisplayPanel
+        String? panelTitle;
+        if (titleWidget != null) {
+          panelTitle = _extractTextFromWidget(titleWidget);
+        }
 
         return TCalendarInherited(
           selected: selected,
@@ -424,14 +416,14 @@ class TCalendar extends StatefulWidget {
             }
           },
           onConfirm: () {
-            result = List.from(selected.value);
+            result = List.from(selected.value);
             onConfirm?.call(result!);
             if (autoClose) {
               doClose(nav);
             }
           },
           child: TPopupBottomDisplayPanel(
-            title: title,
+            title: panelTitle,
             draggable: draggable,
             fixedHeight: panelHeight,
             closeClick: () {
@@ -440,25 +432,21 @@ class TCalendar extends StatefulWidget {
               }
             },
             child: TCalendar(
-              title: title,
+              titleWidget: titleWidget,
               type: type,
-              value: value,
+              initialValue: initialValue,
               minDate: minDate,
               maxDate: maxDate,
               anchorDate: anchorDate,
               firstDayOfWeek: firstDayOfWeek,
-              displayFormat: displayFormat ?? 'year month',
               cellHeight: cellHeight,
               style: style,
-              format: format,
-              bottom: bottom,
-              bottomExpanded: bottomExpanded,
+              popupBottomBuilder: popupBottomBuilder,
+              popupBottomExpanded: popupBottomExpanded,
               onCellClick: onCellClick,
-              onCellLongPress: onCellLongPress,
               cellWidget: cellWidget,
-              dateType: dateType,
+              displayMode: displayMode,
               dataSource: dataSource,
-              showLunarInfo: showLunarInfo,
               onMonthChange: onMonthChange,
               monthTitleBuilder: monthTitleBuilder,
             ),
@@ -471,6 +459,21 @@ class TCalendar extends StatefulWidget {
     return result;
   }
 
+  /// 尝试从 Widget 中提取文本内容,用于 TPopupBottomDisplayPanel 的标题。
+  /// 仅支持 Text 和 RichText 两种常见情况。
+  static String? _extractTextFromWidget(Widget widget) {
+    if (widget is Text) {
+      return widget.data;
+    }
+    if (widget is RichText) {
+      final text = widget.text;
+      if (text is TextSpan) {
+        return text.toPlainText();
+      }
+    }
+    return null;
+  }
+
   @override
   _TCalendarState createState() => _TCalendarState();
 }
@@ -494,6 +497,21 @@ class _TCalendarState extends State {
 
   bool get _usePopupBottom => inherited?.usePopup == true;
 
+  /// 解析 displayMode 为旧的 dateType 和 showLunarInfo
+  TCalendarDateType get _dateType {
+    switch (widget.displayMode) {
+      case TCalendarDisplayMode.solar:
+      case TCalendarDisplayMode.solarWithLunar:
+        return TCalendarDateType.solar;
+      case TCalendarDisplayMode.lunar:
+        return TCalendarDateType.lunar;
+    }
+  }
+
+  bool get _showLunarInfo =>
+      widget.displayMode == TCalendarDisplayMode.solarWithLunar ||
+      widget.displayMode == TCalendarDisplayMode.lunar;
+
   @override
   void didChangeDependencies() {
     super.didChangeDependencies();
@@ -532,7 +550,7 @@ class _TCalendarState extends State {
   @override
   void didUpdateWidget(covariant TCalendar oldWidget) {
     super.didUpdateWidget(oldWidget);
-    if (!listEquals(oldWidget.value, widget.value)) {
+    if (!listEquals(oldWidget.initialValue, widget.initialValue)) {
       _refreshValueCache();
       _syncSelectedToInheritedDeferred();
     }
@@ -544,12 +562,12 @@ class _TCalendarState extends State {
 
   void _assertPopupOnlyBottom() {
     assert(
-      widget.bottom == null || _usePopupBottom,
-      '[TCalendar] bottom 仅能在 TPopupBottomDisplayPanel 内使用',
+      widget.popupBottomBuilder == null || _usePopupBottom,
+      '[TCalendar] popupBottomBuilder 仅能在弹窗模式下使用',
     );
     assert(
-      widget.bottomExpanded == null || _usePopupBottom,
-      '[TCalendar] bottomExpanded 仅能在 TPopupBottomDisplayPanel 内使用',
+      widget.popupBottomExpanded == null || _usePopupBottom,
+      '[TCalendar] popupBottomExpanded 仅能在弹窗模式下使用',
     );
   }
 
@@ -558,7 +576,7 @@ class _TCalendarState extends State {
     if (inherited == null) {
       return;
     }
-    inherited!.selected.value = _getValue(widget.value ?? const []);
+    inherited!.selected.value = _getValue(widget.initialValue ?? const []);
   }
 
   // 适用于 build phase 调用,写操作延迟到下一帧。
@@ -570,7 +588,7 @@ class _TCalendarState extends State {
       if (!mounted || inherited == null) {
         return;
       }
-      inherited!.selected.value = _getValue(widget.value ?? const []);
+      inherited!.selected.value = _getValue(widget.initialValue ?? const []);
     });
   }
 
@@ -578,7 +596,7 @@ class _TCalendarState extends State {
   Widget build(BuildContext context) {
     _assertPopupOnlyBottom();
     final verticalGap = _style.verticalGap ?? TTheme.of(context).spacer8;
-    final hasBottom = widget.bottom != null && _usePopupBottom;
+    final hasBottom = widget.popupBottomBuilder != null && _usePopupBottom;
 
     Widget stackContent(bool bottomExpanded) {
       return Stack(
@@ -590,16 +608,16 @@ class _TCalendarState extends State {
       );
     }
 
-    final child = hasBottom && widget.bottomExpanded != null
+    final child = hasBottom && widget.popupBottomExpanded != null
         ? ValueListenableBuilder(
-            valueListenable: widget.bottomExpanded!,
+            valueListenable: widget.popupBottomExpanded!,
             builder: (context, expanded, _) => stackContent(expanded),
           )
         : stackContent(hasBottom);
 
     return Container(
       height: widget.height ?? _calcInlineDefaultHeight(verticalGap),
-      width: widget.width ?? double.infinity,
+      width: double.infinity,
       decoration: _style.decoration,
       child: child,
     );
@@ -623,21 +641,19 @@ class _TCalendarState extends State {
     return Column(
       children: [
         TCalendarHeader(
-          firstDayOfWeek: widget.firstDayOfWeek ?? 0,
+          firstDayOfWeek: widget.firstDayOfWeek,
           weekdayGap: TTheme.of(context).spacer4,
           padding: TTheme.of(context).spacer16,
           weekdayStyle: _style.weekdayStyle,
           weekdayHeight: 46,
-          title: _showPopupControls ? widget.title : null,
-          titleStyle: _style.titleStyle,
           titleWidget: _showPopupControls ? widget.titleWidget : null,
+          titleStyle: _style.titleStyle,
           titleMaxLine: _style.titleMaxLine,
           titleOverflow: TextOverflow.ellipsis,
           closeBtn: _showPopupControls,
           closeColor: _style.titleCloseColor,
           weekdayNames: weekdayNames,
           onClose: inherited?.onClose,
-          onClick: widget.onHeaderClick,
         ),
         Expanded(
           child: _buildBodyArea(verticalGap, hasBottom, bottomExpanded),
@@ -645,7 +661,7 @@ class _TCalendarState extends State {
         if (_showPopupConfirmBtn)
           inherited?.confirmBtn ??
               Padding(
-                padding: widget.useSafeArea == true
+                padding: widget.safeAreaInset
                     ? EdgeInsets.only(top: TTheme.of(context).spacer16)
                     : EdgeInsets.symmetric(
                         vertical: TTheme.of(context).spacer16),
@@ -657,7 +673,7 @@ class _TCalendarState extends State {
                   onTap: inherited?.onConfirm,
                 ),
               ),
-        if (widget.useSafeArea == true)
+        if (widget.safeAreaInset)
           SizedBox(height: MediaQuery.of(context).padding.bottom)
       ],
     );
@@ -672,7 +688,7 @@ class _TCalendarState extends State {
     if (!hasBottom) {
       return body;
     }
-    if (widget.bottomExpanded == null) {
+    if (widget.popupBottomExpanded == null) {
       return Padding(
         padding: const EdgeInsets.only(bottom: _bottomPeekHeight),
         child: body,
@@ -690,70 +706,67 @@ class _TCalendarState extends State {
 
   Widget _buildCalendarBody(double verticalGap) {
     return TCalendarBody(
-      type: widget.type ?? CalendarType.single,
-      firstDayOfWeek: widget.firstDayOfWeek ?? 0,
+      type: widget.type,
+      firstDayOfWeek: widget.firstDayOfWeek,
       maxDate: widget.maxDate,
       anchorDate: widget.anchorDate,
       minDate: widget.minDate,
       value: _cachedValueDates,
       bodyPadding: _style.bodyPadding ?? TTheme.of(context).spacer16,
-      displayFormat: widget.displayFormat ?? 'year month',
       monthNames: monthNames,
       monthTitleStyle: _style.monthTitleStyle,
       verticalGap: verticalGap,
       cellHeight: _getEffectiveCellHeight(),
-      monthTitleHeight: widget.monthTitleHeight ?? 22,
+      monthTitleHeight: widget.monthTitleHeight,
       monthTitleBuilder: widget.monthTitleBuilder,
-      animateTo: widget.animateTo ?? false,
+      animateTo: widget.animateTo,
       onMonthChange: widget.onMonthChange,
-      dateType: widget.dateType,
+      dateType: _dateType,
       dataSource: widget.dataSource,
       builder: (date, dateList, data, rowIndex, colIndex) {
         return TCalendarCell(
           height: _getEffectiveCellHeight(),
           tdate: date,
-          format: widget.format,
-          type: widget.type ?? CalendarType.single,
+          type: widget.type,
           data: data,
           padding: verticalGap / 2,
           onChange: _handleCellChange,
           onCellClick: widget.onCellClick,
-          onCellLongPress: widget.onCellLongPress,
           dateList: dateList,
           rowIndex: rowIndex,
           colIndex: colIndex,
           cellWidget: widget.cellWidget,
-          dateType: widget.dateType,
-          showLunarInfo: widget.showLunarInfo,
+          dateType: _dateType,
+          showLunarInfo: _showLunarInfo,
         );
       },
     );
   }
 
-  void _handleCellChange(List value) {
-    final time = _getValue(value);
-    inherited?.selected.value = time;
-    widget.onChange?.call(time);
+  void _handleCellChange(List value) {
+    final normalized = _getValue(value);
+    inherited?.selected.value = normalized;
+    widget.onChange?.call(normalized);
   }
 
-  // 行为约定详见 [TCalendar.bottom]。
+  // 行为约定详见 [TCalendar.popupBottomBuilder]。
   Widget _buildBottom(bool bottomExpanded) {
-    assert(widget.bottom != null);
+    assert(widget.popupBottomBuilder != null);
     assert(inherited != null);
 
     final bottomOffset = _calcBottomOffset();
 
-    final content = ValueListenableBuilder>(
+    final content = ValueListenableBuilder>(
       valueListenable: inherited!.selected,
       builder: (context, selectedDates, _) {
-        return widget.bottom!(
+        return widget.popupBottomBuilder!(
           context,
-          List.unmodifiable(selectedDates),
+          List.unmodifiable(selectedDates),
         );
       },
     );
 
-    if (widget.bottomExpanded != null) {
+    if (widget.popupBottomExpanded != null) {
       return Positioned(
         left: 0,
         right: 0,
@@ -780,12 +793,12 @@ class _TCalendarState extends State {
   // 该值需与 build() 中 Column 底部区域(confirmBtn padding + safeArea)保持一致;
   // 修改底部布局时需同步更新本方法。
   double _calcBottomOffset() {
-    final safeBottom = widget.useSafeArea == true
+    final safeBottom = widget.safeAreaInset
         ? MediaQuery.of(context).padding.bottom
         : 0.0;
 
     if (_showPopupConfirmBtn) {
-      final btnPadding = widget.useSafeArea == true
+      final btnPadding = widget.safeAreaInset
           ? TTheme.of(context).spacer16
           : TTheme.of(context).spacer16 * 2;
       // 仅默认确认按钮使用固定高度;自定义 confirmBtn 由调用方保证 bottom 不重叠。
@@ -797,18 +810,15 @@ class _TCalendarState extends State {
     return safeBottom;
   }
 
-  List _getValue(List value) {
-    return value.map((e) {
-      final date = DateTime.fromMillisecondsSinceEpoch(e);
-      return DateTime(date.year, date.month, date.day).millisecondsSinceEpoch;
-    }).toList();
+  List _getValue(List value) {
+    return value.map((e) => DateTime(e.year, e.month, e.day)).toList();
   }
 
   double _getEffectiveCellHeight() {
     if (widget.cellHeight != null) {
       return widget.cellHeight!;
     }
-    return widget.showLunarInfo ? 80 : 60;
+    return 60;
   }
 
   /// 内嵌模式下不传 `height` 时的默认高度。
@@ -816,7 +826,7 @@ class _TCalendarState extends State {
   /// 布局 = weekday(46) + monthTitle(22) + 5行(cellHeight + verticalGap) + bodyPadding*2
   double _calcInlineDefaultHeight(double verticalGap) {
     const weekdayHeight = 46.0;
-    final monthTitleHeight = widget.monthTitleHeight ?? 22.0;
+    final monthTitleHeight = widget.monthTitleHeight;
     final cellHeight = _getEffectiveCellHeight();
     final bodyPadding = _style.bodyPadding ?? TTheme.of(context).spacer16;
     const visibleRows = 5;
diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
index 38b915d4b..d992802c5 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
 import '../../../tdesign_flutter.dart';
 import '../../util/context_extension.dart';
 import '../../util/iterable_ext.dart';
+import 't_calendar_cell.dart';
 
 class TCalendarBody extends StatefulWidget {
   const TCalendarBody({
@@ -13,7 +14,6 @@ class TCalendarBody extends StatefulWidget {
     required this.firstDayOfWeek,
     required this.builder,
     required this.bodyPadding,
-    required this.displayFormat,
     required this.monthNames,
     this.monthTitleStyle,
     this.monthTitleBuilder,
@@ -27,8 +27,8 @@ class TCalendarBody extends StatefulWidget {
     this.dataSource,
   }) : super(key: key);
 
-  final int? maxDate;
-  final int? minDate;
+  final DateTime? maxDate;
+  final DateTime? minDate;
   final CalendarType type;
   final List? value;
   final DateTime? anchorDate;
@@ -41,7 +41,6 @@ class TCalendarBody extends StatefulWidget {
     int colIndex,
   ) builder;
   final double bodyPadding;
-  final String displayFormat;
   final List monthNames;
   final TextStyle? monthTitleStyle;
   final Widget Function(
@@ -115,8 +114,8 @@ class _TCalendarBodyState extends State {
       final mh = _getMonthHeight(_months, i, _monthHeight);
       if (_scrollController.offset >= currentOffset &&
           _scrollController.offset < currentOffset + mh) {
-        if (i + 1 < _months.length) {
-          final currentMonth = _months[i + 1];
+        if (i < _months.length) {
+          final currentMonth = _months[i];
           if (_lastPrintMonth == null ||
               !_lastPrintMonth!.isAtSameMomentAs(currentMonth)) {
             _lastPrintMonth = currentMonth;
@@ -161,9 +160,7 @@ class _TCalendarBodyState extends State {
         final monthDate = _months[index];
         final monthYear = monthDate.year.toString() + context.resource.year;
         final monthMonth = widget.monthNames[monthDate.month - 1];
-        final monthDateText = widget.displayFormat
-            .replaceFirst('year', monthYear)
-            .replaceFirst('month', monthMonth);
+        final monthDateText = '$monthYear $monthMonth';
         late List monthData;
         if (_data.containsKey(monthDate)) {
           monthData = _data[monthDate]!;
@@ -255,10 +252,9 @@ class _TCalendarBodyState extends State {
     });
   }
 
-  DateTime _getDefDate(int? date, [bool isMax = false]) {
+  DateTime _getDefDate(DateTime? date, [bool isMax = false]) {
     if (date != null) {
-      final d = DateTime.fromMillisecondsSinceEpoch(date);
-      return DateTime(d.year, d.month, d.day);
+      return DateTime(date.year, date.month, date.day);
     }
     return isMax ? DateTime(2100, 12, 31) : DateTime(1970);
   }
diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart b/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart
index 5ab160935..8b1a92db8 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart
@@ -7,10 +7,8 @@ class TCalendarCell extends StatefulWidget {
   const TCalendarCell({
     Key? key,
     this.tdate,
-    this.format,
     required this.type,
     this.onCellClick,
-    this.onCellLongPress,
     this.onChange,
     required this.height,
     required this.data,
@@ -24,19 +22,13 @@ class TCalendarCell extends StatefulWidget {
   }) : super(key: key);
 
   final TDate? tdate;
-  final CalendarFormat? format;
   final CalendarType type;
   final void Function(
-    int value,
-    DateSelectType type,
+    DateTime value,
+    DateSelectType selectType,
     TDate tdate,
   )? onCellClick;
-  final void Function(
-    int value,
-    DateSelectType type,
-    TDate tdate,
-  )? onCellLongPress;
-  final void Function(List value)? onChange;
+  final void Function(List value)? onChange;
   final double height;
   final Map> data;
   final double padding;
@@ -87,7 +79,7 @@ class _TCalendarCellState extends State {
     if (widget.tdate == null) {
       return const SizedBox.shrink();
     }
-    final tdate = widget.format?.call(widget.tdate) ?? widget.tdate!;
+    final tdate = widget.tdate!;
     final cellStyle = TCalendarStyle.cellStyle(context, widget.tdate!._type);
     final decoration = tdate.decoration ?? cellStyle.cellDecoration;
     final positionColor = _getColor(cellStyle, decoration);
@@ -100,11 +92,6 @@ class _TCalendarCellState extends State {
     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: [
@@ -133,7 +120,7 @@ class _TCalendarCellState extends State {
   void _cellTap() {
     final list = widget.data.values.expand((element) => element).toList();
     final selectType = widget.tdate!._type;
-    final curDate = widget.tdate!._milliseconds;
+    final curDate = widget.tdate!.date;
     if (selectType == DateSelectType.disabled) {
       widget.onCellClick?.call(curDate, selectType, widget.tdate!);
       return;
@@ -144,7 +131,7 @@ class _TCalendarCellState extends State {
             list.find((item) => item?._type == DateSelectType.selected);
         date?._setType(DateSelectType.empty);
         widget.tdate!._setType(DateSelectType.selected);
-        if (date?._milliseconds != curDate) {
+        if (date?.date != curDate) {
           widget.onChange?.call([curDate]);
         }
         break;
@@ -152,8 +139,8 @@ class _TCalendarCellState extends State {
         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) {
+        final value = date.map((item) => item!.date).toList();
+        if (date.find((item) => item!.date == curDate) != null) {
           widget.tdate!._setType(DateSelectType.empty);
           value.remove(curDate);
         } else {
@@ -165,10 +152,10 @@ class _TCalendarCellState extends State {
       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;
+        final startDate = start?.date;
         if ((start == null && end == null) ||
             (start != null && end != null) ||
-            (start != null && end == null && startTimes! >= curDate)) {
+            (start != null && end == null && !startDate!.isBefore(curDate))) {
           start?._setType(DateSelectType.empty);
           end?._setType(DateSelectType.empty);
           final centres = list
@@ -177,16 +164,16 @@ class _TCalendarCellState extends State {
           centres.forEach((item) => item!._setType(DateSelectType.empty));
           widget.tdate!._setType(DateSelectType.start);
           widget.onChange?.call([curDate]);
-        } else if (start != null && end == null && startTimes! < curDate) {
+        } else if (start != null && end == null && startDate!.isBefore(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]!.date.isBefore(curDate)) {
             list[startIndex]?._setType(DateSelectType.centre);
             startIndex++;
           }
-          widget.onChange?.call([startTimes, curDate]);
+          widget.onChange?.call([startDate, curDate]);
         }
         break;
     }
@@ -226,8 +213,8 @@ class _TCalendarCellState extends State {
 
   bool _isToday() {
     final today = DateTime.now();
-    return widget.tdate?._milliseconds ==
-        DateTime(today.year, today.month, today.day).millisecondsSinceEpoch;
+    return widget.tdate?.date ==
+        DateTime(today.year, today.month, today.day);
   }
 
   /// 构建默认单元格内容
@@ -383,9 +370,6 @@ class TDate {
   /// name: 假期名称
   final Map? holidayInfo;
 
-  int get _milliseconds =>
-      DateTime(date.year, date.month, date.day).millisecondsSinceEpoch;
-
   DateSelectType get _type => typeNotifier.value;
 
   void _setType(DateSelectType type) {
diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_header.dart b/tdesign-component/lib/src/components/calendar/t_calendar_header.dart
index 92e259ab4..d6105568d 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar_header.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar_header.dart
@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import '../../../tdesign_flutter.dart';
+import 't_calendar_cell.dart';
 
 class TCalendarHeader extends StatelessWidget {
   const TCalendarHeader({
@@ -9,15 +10,13 @@ class TCalendarHeader extends StatelessWidget {
     required this.padding,
     this.weekdayStyle,
     required this.weekdayHeight,
-    this.title,
-    this.titleStyle,
     this.titleWidget,
+    this.titleStyle,
     this.titleMaxLine,
     this.titleOverflow,
     this.closeBtn = true,
     this.closeColor,
     this.onClose,
-    this.onClick,
     required this.weekdayNames,
   }) : super(key: key);
 
@@ -26,16 +25,14 @@ class TCalendarHeader extends StatelessWidget {
   final double padding;
   final TextStyle? weekdayStyle;
   final double weekdayHeight;
-  final String? title;
-  final TextStyle? titleStyle;
   final Widget? titleWidget;
+  final TextStyle? titleStyle;
   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 = [];
@@ -54,7 +51,7 @@ class TCalendarHeader extends StatelessWidget {
       padding: EdgeInsets.symmetric(horizontal: padding),
       child: Column(
         children: [
-          if (title?.isNotEmpty == true || titleWidget != null || closeBtn)
+          if (titleWidget != null || closeBtn)
             Container(
               padding: EdgeInsets.symmetric(vertical: padding),
               child: Row(
@@ -62,13 +59,7 @@ class TCalendarHeader extends StatelessWidget {
                   if (closeBtn) const SizedBox(width: 24),
                   Expanded(
                     child: Center(
-                      child: titleWidget ??
-                          TText(
-                            title,
-                            style: titleStyle,
-                            maxLines: titleMaxLine,
-                            overflow: TextOverflow.ellipsis,
-                          ),
+                      child: titleWidget ?? const SizedBox.shrink(),
                     ),
                   ),
                   if (closeBtn)
@@ -89,17 +80,12 @@ class TCalendarHeader extends StatelessWidget {
               for (int index = 0; index < list.length; index++) ...[
                 if (index != 0) SizedBox(width: weekdayGap),
                 Expanded(
-                  child: GestureDetector(
-                    onTap: () {
-                      onClick?.call(index, list[index]);
-                    },
-                    child: SizedBox(
-                      height: weekdayHeight,
-                      child: Center(
-                        child: TText(
-                          list[index],
-                          style: weekdayStyle,
-                        ),
+                  child: SizedBox(
+                    height: weekdayHeight,
+                    child: Center(
+                      child: TText(
+                        list[index],
+                        style: weekdayStyle,
                       ),
                     ),
                   ),
diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_style.dart b/tdesign-component/lib/src/components/calendar/t_calendar_style.dart
index 5dba6ab1c..a1f23925f 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar_style.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar_style.dart
@@ -20,10 +20,10 @@ class TCalendarStyle {
 
   BoxDecoration? decoration;
 
-  /// header区域 [TCalendar.title]的样式
+  /// header区域 [TCalendar.titleWidget]的样式
   TextStyle? titleStyle;
 
-  /// header区域 [TCalendar.title]的行数
+  /// header区域 [TCalendar.titleWidget]的行数
   int? titleMaxLine;
 
   /// header区域 关闭图标的颜色
diff --git a/tdesign-component/lib/src/theme/resource_delegate.dart b/tdesign-component/lib/src/theme/resource_delegate.dart
index f0a020a12..bd7f34e0b 100644
--- a/tdesign-component/lib/src/theme/resource_delegate.dart
+++ b/tdesign-component/lib/src/theme/resource_delegate.dart
@@ -121,64 +121,64 @@ abstract class TResourceDelegate {
   /// 周
   String get weeksLabel;
 
-  /// [TCalendarHeader] 星期日
+  /// [TCalendar] 星期日
   String get sunday;
 
-  /// [TCalendarHeader] 星期一
+  /// [TCalendar] 星期一
   String get monday;
 
-  /// [TCalendarHeader] 星期二
+  /// [TCalendar] 星期二
   String get tuesday;
 
-  /// [TCalendarHeader] 星期三
+  /// [TCalendar] 星期三
   String get wednesday;
 
-  /// [TCalendarHeader] 星期四
+  /// [TCalendar] 星期四
   String get thursday;
 
-  /// [TCalendarHeader] 星期五
+  /// [TCalendar] 星期五
   String get friday;
 
-  /// [TCalendarHeader] 星期六
+  /// [TCalendar] 星期六
   String get saturday;
 
-  /// [TCalendarBody] 年
+  /// [TCalendar] 年
   String get year;
 
-  /// [TCalendarBody] 一月
+  /// [TCalendar] 一月
   String get january;
 
-  /// [TCalendarBody] 二月
+  /// [TCalendar] 二月
   String get february;
 
-  /// [TCalendarBody] 三月
+  /// [TCalendar] 三月
   String get march;
 
-  /// [TCalendarBody] 四月
+  /// [TCalendar] 四月
   String get april;
 
-  /// [TCalendarBody] 五月
+  /// [TCalendar] 五月
   String get may;
 
-  /// [TCalendarBody] 六月
+  /// [TCalendar] 六月
   String get june;
 
-  /// [TCalendarBody] 七月
+  /// [TCalendar] 七月
   String get july;
 
-  /// [TCalendarBody] 八月
+  /// [TCalendar] 八月
   String get august;
 
-  /// [TCalendarBody] 九月
+  /// [TCalendar] 九月
   String get september;
 
-  /// [TCalendarBody] 十月
+  /// [TCalendar] 十月
   String get october;
 
-  /// [TCalendarBody] 十一月
+  /// [TCalendar] 十一月
   String get november;
 
-  /// [TCalendarBody] 十二月
+  /// [TCalendar] 十二月
   String get december;
 
   /// [TCalendar] 时间

From 6d21114a1f4cfb80328aea44642a6ce38e13bfb1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= 
Date: Wed, 20 May 2026 03:11:33 +0800
Subject: [PATCH 16/35] =?UTF-8?q?fix(calendar):=20=E4=BF=AE=E5=A4=8D?=
 =?UTF-8?q?=E6=97=A5=E5=8E=86=E8=B6=8A=E7=95=8C=E5=8F=8A=E5=8A=A8=E7=94=BB?=
 =?UTF-8?q?=E8=B7=B3=E5=8A=A8=E5=B9=B6=E9=87=8D=E6=9E=84=E6=8E=A7=E5=88=B6?=
 =?UTF-8?q?=E6=A0=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../example/lib/page/t_calendar_page.dart     | 394 +++++++++++-------
 .../components/calendar/t_calendar_body.dart  |  92 ++--
 2 files changed, 307 insertions(+), 179 deletions(-)

diff --git a/tdesign-component/example/lib/page/t_calendar_page.dart b/tdesign-component/example/lib/page/t_calendar_page.dart
index ab763a9bc..b4922c1d5 100644
--- a/tdesign-component/example/lib/page/t_calendar_page.dart
+++ b/tdesign-component/example/lib/page/t_calendar_page.dart
@@ -1,3 +1,5 @@
+import 'dart:async';
+
 import 'package:flutter/material.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import '../../base/example_widget.dart';
@@ -1053,15 +1055,18 @@ class _LunarCalendarDemo extends StatefulWidget {
 class _LunarCalendarDemoState extends State<_LunarCalendarDemo> {
   final _dataSource = LunarDataSourceExample();
 
-  late DateTime _anchorDate;
+  // 日历可用日期范围。年份/月份选择器都基于这两个常量约束,
+  // 避免越界导致组件层 clamp 兜底(视觉上会卡在端点月份)。
+  static final DateTime _minDate = DateTime(2020, 1, 1);
+  static final DateTime _maxDate = DateTime(2030, 12, 31);
+
+  DateTime? _anchorDate;
   late TCalendarDisplayMode _displayMode;
   List _selected = [DateTime.now()];
 
   @override
   void initState() {
     super.initState();
-    final now = DateTime.now();
-    _anchorDate = DateTime(now.year, now.month, 15);
     _displayMode = TCalendarDisplayMode.solarWithLunar;
   }
 
@@ -1074,8 +1079,12 @@ class _LunarCalendarDemoState extends State<_LunarCalendarDemo> {
         _LunarControlBar(
           key: _LunarControlBar.monthKey,
           dataSource: _dataSource,
+          minDate: _minDate,
+          maxDate: _maxDate,
           onNavigate: (anchor) {
-            setState(() => _anchorDate = anchor);
+            setState(() {
+              _anchorDate = anchor;
+            });
           },
           onModeChanged: (mode) {
             setState(() => _displayMode = mode);
@@ -1085,8 +1094,8 @@ class _LunarCalendarDemoState extends State<_LunarCalendarDemo> {
         // ---- 内嵌日历 ----
         TCalendar(
           type: CalendarType.single,
-          minDate: DateTime(2020, 1, 1),
-          maxDate: DateTime(2030, 12, 31),
+          minDate: _minDate,
+          maxDate: _maxDate,
           initialValue: _selected,
           anchorDate: _anchorDate,
           animateTo: true,
@@ -1149,11 +1158,15 @@ class _LunarControlBar extends StatefulWidget {
   const _LunarControlBar({
     super.key,
     required this.dataSource,
+    required this.minDate,
+    required this.maxDate,
     required this.onNavigate,
     required this.onModeChanged,
   });
 
   final LunarDataSourceExample dataSource;
+  final DateTime minDate;
+  final DateTime maxDate;
   final ValueChanged onNavigate;
   final ValueChanged onModeChanged;
 
@@ -1169,54 +1182,228 @@ class _LunarControlBarState extends State<_LunarControlBar> {
   late DateTime _currentMonth;
   late TCalendarDisplayMode _mode;
 
-  // 使用 ValueNotifier 避免滚动时 setState 导致整个控制栏重建和布局重算
-  final _yearTextNotifier = ValueNotifier('');
-  final _monthTextNotifier = ValueNotifier('');
-  final _lunarTextNotifier = ValueNotifier('');
+  /// 主动跳转期间忽略来自日历 onMonthChange 的中间过渡回调。
+  ///
+  /// 背景:日历内部 animateTo(约 200ms)会沿途触发若干次 onMonthChange,
+  /// 例如从 2026-05 跳到 2023-05,期间会快速地经过 36 个月份;
+  /// 若让控制栏跟随这些中间值刷新,用户视觉上会看到年/月数字"滚屏跳动"。
+  ///
+  /// 因此当用户主动 _navigateTo 时,短暂屏蔽 updateMonth 的中间值,
+  /// 等动画结束后再恢复随手势/上层切换的同步。
+  bool _suppressUpdate = false;
+  Timer? _suppressTimer;
+
+  // 略微大于动画时长(200ms),留出一帧抖动余量。
+  static const Duration _suppressDuration = Duration(milliseconds: 280);
 
   @override
   void initState() {
     super.initState();
     final now = DateTime.now();
-    _currentMonth = DateTime(now.year, now.month, 1);
+    _currentMonth = _clampMonth(DateTime(now.year, now.month, 1));
     _mode = TCalendarDisplayMode.solarWithLunar;
-    _updateNotifiers();
   }
 
   @override
   void dispose() {
-    _yearTextNotifier.dispose();
-    _monthTextNotifier.dispose();
-    _lunarTextNotifier.dispose();
+    _suppressTimer?.cancel();
     super.dispose();
   }
 
-  void _updateNotifiers() {
-    _yearTextNotifier.value = '${_currentMonth.year}年';
-    _monthTextNotifier.value = '${_currentMonth.month}月';
-    final lunarInfo = widget.dataSource.getLunarInfo(_currentMonth);
-    _lunarTextNotifier.value = lunarInfo != null
-        ? '${lunarInfo.yearText}年 ${lunarInfo.monthText}'
-        : '';
+  /// 将任意 (year, month) clamp 到 [minDate, maxDate] 区间内。
+  DateTime _clampMonth(DateTime date) {
+    final minKey = widget.minDate.year * 12 + widget.minDate.month;
+    final maxKey = widget.maxDate.year * 12 + widget.maxDate.month;
+    final key = (date.year * 12 + date.month).clamp(minKey, maxKey);
+    final year = (key - 1) ~/ 12;
+    final month = (key - 1) % 12 + 1;
+    return DateTime(year, month, 1);
+  }
+
+  bool _canGoPrev() {
+    final cur = _currentMonth.year * 12 + _currentMonth.month;
+    final minKey = widget.minDate.year * 12 + widget.minDate.month;
+    return cur > minKey;
+  }
+
+  bool _canGoNext() {
+    final cur = _currentMonth.year * 12 + _currentMonth.month;
+    final maxKey = widget.maxDate.year * 12 + widget.maxDate.month;
+    return cur < maxKey;
   }
 
-  /// 由日历 onMonthChange 回调驱动,仅更新 ValueNotifier,不触发 setState
+  /// 由日历 onMonthChange 回调驱动,仅更新显示
   void updateMonth(DateTime month) {
+    // 主动跳转动画过程中沿途触发的中间月份不要应用,避免数字跳动。
+    if (_suppressUpdate) {
+      return;
+    }
     if (_currentMonth.year == month.year && _currentMonth.month == month.month) {
       return;
     }
-    _currentMonth = month;
-    _updateNotifiers();
+    setState(() => _currentMonth = month);
   }
 
   void _navigateTo(DateTime month) {
-    _currentMonth = month;
-    _updateNotifiers();
-    widget.onNavigate(DateTime(month.year, month.month, 15));
+    final clamped = _clampMonth(month);
+    // 命中相同月份时直接返回,避免触发上层无意义重建。
+    if (_currentMonth.year == clamped.year &&
+        _currentMonth.month == clamped.month) {
+      return;
+    }
+    // 1) 立即更新控制栏显示,用户感知零延迟
+    setState(() => _currentMonth = clamped);
+    // 2) 在动画期间屏蔽来自日历的中间月份回调,防止数字跳动
+    _suppressUpdate = true;
+    _suppressTimer?.cancel();
+    _suppressTimer = Timer(_suppressDuration, () {
+      if (!mounted) {
+        return;
+      }
+      _suppressUpdate = false;
+    });
+    // 3) 通知上层触发日历滚动
+    widget.onNavigate(DateTime(clamped.year, clamped.month, 15));
+  }
+
+  Future _pickYear() async {
+    final minYear = widget.minDate.year;
+    final maxYear = widget.maxDate.year;
+    final count = maxYear - minYear + 1;
+    final selectedIndex = _currentMonth.year - minYear;
+    // 让选中项默认居中(每行约 56 dp,参考 ListTile 默认高度)。
+    const itemExtent = 56.0;
+    final controller = ScrollController(
+      initialScrollOffset: (selectedIndex * itemExtent - 120).clamp(
+        0.0,
+        (count * itemExtent - 200).clamp(0.0, double.infinity),
+      ),
+    );
+    final year = await showModalBottomSheet(
+      context: context,
+      builder: (ctx) {
+        return SizedBox(
+          height: 300,
+          child: Column(
+            children: [
+              const Padding(
+                padding: EdgeInsets.all(16),
+                child: Text('选择年份',
+                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
+              ),
+              Expanded(
+                child: ListView.builder(
+                  controller: controller,
+                  itemExtent: itemExtent,
+                  itemCount: count,
+                  itemBuilder: (ctx, index) {
+                    final y = minYear + index;
+                    final isSelected = y == _currentMonth.year;
+                    return ListTile(
+                      title: Text('$y年',
+                          style: TextStyle(
+                            color: isSelected ? Colors.blue : null,
+                            fontWeight:
+                                isSelected ? FontWeight.bold : null,
+                          )),
+                      trailing: isSelected
+                          ? const Icon(Icons.check, color: Colors.blue)
+                          : null,
+                      onTap: () => Navigator.pop(ctx, y),
+                    );
+                  },
+                ),
+              ),
+            ],
+          ),
+        );
+      },
+    );
+    controller.dispose();
+    if (year != null) {
+      // 切年时,目标月可能在端点年越界,_navigateTo 内部会兜底 clamp。
+      _navigateTo(DateTime(year, _currentMonth.month, 1));
+    }
+  }
+
+  Future _pickMonth() async {
+    final minKey = widget.minDate.year * 12 + widget.minDate.month;
+    final maxKey = widget.maxDate.year * 12 + widget.maxDate.month;
+    final m = await showModalBottomSheet(
+      context: context,
+      builder: (ctx) {
+        return SizedBox(
+          height: 400,
+          child: Column(
+            children: [
+              const Padding(
+                padding: EdgeInsets.all(16),
+                child: Text('选择月份',
+                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
+              ),
+              Expanded(
+                child: GridView.builder(
+                  padding: const EdgeInsets.all(16),
+                  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+                    crossAxisCount: 3,
+                    childAspectRatio: 2,
+                    crossAxisSpacing: 10,
+                    mainAxisSpacing: 10,
+                  ),
+                  itemCount: 12,
+                  itemBuilder: (ctx, index) {
+                    final month = index + 1;
+                    final monthKey = _currentMonth.year * 12 + month;
+                    final isDisabled =
+                        monthKey < minKey || monthKey > maxKey;
+                    final isSelected =
+                        !isDisabled && month == _currentMonth.month;
+                    final bgColor = isDisabled
+                        ? Colors.grey.shade100
+                        : (isSelected ? Colors.blue : Colors.grey.shade200);
+                    final fgColor = isDisabled
+                        ? Colors.grey.shade400
+                        : (isSelected ? Colors.white : Colors.black);
+                    return InkWell(
+                      onTap: isDisabled
+                          ? null
+                          : () => Navigator.pop(ctx, month),
+                      child: Container(
+                        alignment: Alignment.center,
+                        decoration: BoxDecoration(
+                          color: bgColor,
+                          borderRadius: BorderRadius.circular(8),
+                        ),
+                        child: Text('$month月',
+                            style: TextStyle(
+                              color: fgColor,
+                              fontWeight:
+                                  isSelected ? FontWeight.bold : null,
+                            )),
+                      ),
+                    );
+                  },
+                ),
+              ),
+            ],
+          ),
+        );
+      },
+    );
+    if (m != null) {
+      _navigateTo(DateTime(_currentMonth.year, m, 1));
+    }
   }
 
   @override
   Widget build(BuildContext context) {
+    final lunarInfo = widget.dataSource.getLunarInfo(_currentMonth);
+    final lunarMonth = lunarInfo != null
+        ? '${lunarInfo.yearText}年 ${lunarInfo.monthText}'
+        : '';
+    final canPrev = _canGoPrev();
+    final canNext = _canGoNext();
+
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
       child: Column(
@@ -1225,19 +1412,12 @@ class _LunarControlBarState extends State<_LunarControlBar> {
           // 固定高度容器,防止农历文字有无时布局跳动
           SizedBox(
             height: 20,
-            child: ValueListenableBuilder(
-              valueListenable: _lunarTextNotifier,
-              builder: (context, lunarMonth, _) {
-                if (lunarMonth.isEmpty) return const SizedBox.shrink();
-                return Padding(
-                  padding: const EdgeInsets.only(bottom: 4),
-                  child: Text(
+            child: lunarMonth.isNotEmpty
+                ? Text(
                     lunarMonth,
                     style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
-                  ),
-                );
-              },
-            ),
+                  )
+                : const SizedBox.shrink(),
           ),
           Row(
             children: [
@@ -1245,119 +1425,28 @@ class _LunarControlBarState extends State<_LunarControlBar> {
                 text: '◀',
                 size: TButtonSize.small,
                 theme: TButtonTheme.defaultTheme,
-                onTap: () => _navigateTo(DateTime(_currentMonth.year, _currentMonth.month - 1, 1)),
+                disabled: !canPrev,
+                onTap: canPrev
+                    ? () => _navigateTo(DateTime(
+                        _currentMonth.year, _currentMonth.month - 1, 1))
+                    : null,
               ),
               const SizedBox(width: 4),
               Expanded(
-                child: ValueListenableBuilder(
-                  valueListenable: _yearTextNotifier,
-                  builder: (context, text, _) => TButton(
-                    text: text,
-                    size: TButtonSize.small,
-                    theme: TButtonTheme.defaultTheme,
-                    onTap: () async {
-                      final year = await showModalBottomSheet(
-                        context: context,
-                        builder: (context) {
-                          return SizedBox(
-                            height: 300,
-                            child: Column(
-                              children: [
-                                const Padding(
-                                  padding: EdgeInsets.all(16),
-                                  child: Text('选择年份',
-                                      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
-                                ),
-                                Expanded(
-                                  child: ListView.builder(
-                                    itemCount: 50,
-                                    itemBuilder: (context, index) {
-                                      final y = DateTime.now().year - 10 + index;
-                                      return ListTile(
-                                        title: Text('$y年',
-                                            style: TextStyle(
-                                              color: y == _currentMonth.year ? Colors.blue : null,
-                                              fontWeight: y == _currentMonth.year ? FontWeight.bold : null,
-                                            )),
-                                        onTap: () => Navigator.pop(context, y),
-                                      );
-                                    },
-                                  ),
-                                ),
-                              ],
-                            ),
-                          );
-                        },
-                      );
-                      if (year != null) {
-                        _navigateTo(DateTime(year, _currentMonth.month, 1));
-                      }
-                    },
-                  ),
+                child: TButton(
+                  text: '${_currentMonth.year}年',
+                  size: TButtonSize.small,
+                  theme: TButtonTheme.defaultTheme,
+                  onTap: _pickYear,
                 ),
               ),
               const SizedBox(width: 4),
               Expanded(
-                child: ValueListenableBuilder(
-                  valueListenable: _monthTextNotifier,
-                  builder: (context, text, _) => TButton(
-                    text: text,
-                    size: TButtonSize.small,
-                    theme: TButtonTheme.defaultTheme,
-                    onTap: () async {
-                      final m = await showModalBottomSheet(
-                        context: context,
-                        builder: (context) {
-                          return SizedBox(
-                            height: 400,
-                            child: Column(
-                              children: [
-                                const Padding(
-                                  padding: EdgeInsets.all(16),
-                                  child: Text('选择月份',
-                                      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
-                                ),
-                                Expanded(
-                                  child: GridView.builder(
-                                    padding: const EdgeInsets.all(16),
-                                    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
-                                      crossAxisCount: 3,
-                                      childAspectRatio: 2,
-                                      crossAxisSpacing: 10,
-                                      mainAxisSpacing: 10,
-                                    ),
-                                    itemCount: 12,
-                                    itemBuilder: (context, index) {
-                                      final m = index + 1;
-                                      final isSelected = m == _currentMonth.month;
-                                      return InkWell(
-                                        onTap: () => Navigator.pop(context, m),
-                                        child: Container(
-                                          alignment: Alignment.center,
-                                          decoration: BoxDecoration(
-                                            color: isSelected ? Colors.blue : Colors.grey.shade200,
-                                            borderRadius: BorderRadius.circular(8),
-                                          ),
-                                          child: Text('$m月',
-                                              style: TextStyle(
-                                                color: isSelected ? Colors.white : Colors.black,
-                                                fontWeight: isSelected ? FontWeight.bold : null,
-                                              )),
-                                        ),
-                                      );
-                                    },
-                                  ),
-                                ),
-                              ],
-                            ),
-                          );
-                        },
-                      );
-                      if (m != null) {
-                        _navigateTo(DateTime(_currentMonth.year, m, 1));
-                      }
-                    },
-                  ),
+                child: TButton(
+                  text: '${_currentMonth.month}月',
+                  size: TButtonSize.small,
+                  theme: TButtonTheme.defaultTheme,
+                  onTap: _pickMonth,
                 ),
               ),
               const SizedBox(width: 4),
@@ -1365,12 +1454,18 @@ class _LunarControlBarState extends State<_LunarControlBar> {
                 text: '▶',
                 size: TButtonSize.small,
                 theme: TButtonTheme.defaultTheme,
-                onTap: () => _navigateTo(DateTime(_currentMonth.year, _currentMonth.month + 1, 1)),
+                disabled: !canNext,
+                onTap: canNext
+                    ? () => _navigateTo(DateTime(
+                        _currentMonth.year, _currentMonth.month + 1, 1))
+                    : null,
               ),
               const SizedBox(width: 8),
               Text('农历', style: TextStyle(fontSize: 12, color: Colors.grey.shade700)),
-              Switch(
-                value: _mode == TCalendarDisplayMode.solarWithLunar ||
+              const SizedBox(width: 4),
+              TSwitch(
+                size: TSwitchSize.small,
+                isOn: _mode == TCalendarDisplayMode.solarWithLunar ||
                     _mode == TCalendarDisplayMode.lunar,
                 onChanged: (v) {
                   final newMode = v
@@ -1378,8 +1473,9 @@ class _LunarControlBarState extends State<_LunarControlBar> {
                       : TCalendarDisplayMode.solar;
                   setState(() => _mode = newMode);
                   widget.onModeChanged(newMode);
+                  // 已自行处理状态,返回 true 阻止 TSwitch 内部刷新(避免双源冲突)
+                  return true;
                 },
-                materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
               ),
             ],
           ),
diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
index d992802c5..77c4f0701 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
@@ -71,10 +71,10 @@ class _TCalendarBodyState extends State {
   @override
   void initState() {
     super.initState();
-    _scrollController = ScrollController();
-    _scrollController.addListener(_onScroll);
     _initMonths();
-    _scrollToItem();
+    final initialOffset = _calcScrollOffset();
+    _scrollController = ScrollController(initialScrollOffset: initialOffset);
+    _scrollController.addListener(_onScroll);
   }
 
   @override
@@ -87,8 +87,11 @@ class _TCalendarBodyState extends State {
       _lastPrintMonth = null;
       _initMonths();
     }
-    if (oldWidget.anchorDate != widget.anchorDate ||
-        oldWidget.value != widget.value) {
+    // anchorDate 变化时滚动:使用 identical 引用比较,
+    // 让上层即使重复传同一年月(例如滑动到该月后再点击该月按钮)也能触发滚动;
+    // 上层只要每次导航都构造新的 DateTime 实例即可。
+    final newAnchor = widget.anchorDate;
+    if (newAnchor != null && !identical(newAnchor, oldWidget.anchorDate)) {
       _scrollToItem();
     }
   }
@@ -106,6 +109,40 @@ class _TCalendarBodyState extends State {
     _months = _monthsBetween(_min, _max);
   }
 
+  /// 计算目标日期所在月份的滚动偏移量
+  ///
+  /// 越界处理策略:
+  /// - 早于 minDate → clamp 到第一个月
+  /// - 晚于 maxDate → clamp 到最后一个月
+  /// 这样上层即使传入越界 anchorDate,也不会出现"静默回到顶部"的异常表现。
+  double _calcScrollOffset() {
+    var scrollToDate = widget.anchorDate;
+    if (scrollToDate == null) {
+      if (widget.value == null || widget.value!.isEmpty) {
+        return 0.0;
+      }
+      scrollToDate = widget.value!.reduce((a, b) => a.isBefore(b) ? a : b);
+    }
+    if (_months.isEmpty) {
+      return 0.0;
+    }
+    // 用 (年*12 + 月) 作为可比较的标量,规避日级别比较带来的边界陷阱。
+    final firstKey = _months.first.year * 12 + _months.first.month;
+    final lastKey = _months.last.year * 12 + _months.last.month;
+    final targetKey = scrollToDate.year * 12 + scrollToDate.month;
+    final clampedKey = targetKey.clamp(firstKey, lastKey);
+    var height = 0.0;
+    for (var i = 0; i < _months.length; i++) {
+      final item = _months[i];
+      final itemKey = item.year * 12 + item.month;
+      if (itemKey == clampedKey) {
+        break;
+      }
+      height += _getMonthHeight(_months, i, _monthHeight);
+    }
+    return height;
+  }
+
   int? _lastCleanupIndex;
 
   void _onScroll() {
@@ -211,44 +248,39 @@ class _TCalendarBodyState extends State {
   }
 
   void _scrollToItem() {
-    var scrollToDate = widget.anchorDate;
-    if (scrollToDate == null) {
-      if (widget.value == null || widget.value!.isEmpty) {
-        return;
-      }
-      scrollToDate = widget.value!.reduce((a, b) => a.isBefore(b) ? a : b);
-    }
-    var lastMonthDay = DateTime(_months.last.year, _months.last.month + 1);
-    lastMonthDay = lastMonthDay.add(const Duration(days: -1));
-    if (_months.first.isAfter(scrollToDate) ||
-        lastMonthDay.isBefore(scrollToDate)) {
-      return;
-    }
-    final targetDate = scrollToDate;
-    WidgetsBinding.instance.addPostFrameCallback((_) {
+    final height = _calcScrollOffset();
+    // 等待 ScrollController 完成 attach 后再滚动,最多重试若干帧。
+    void attemptScroll([int retry = 0]) {
       if (!mounted) {
         return;
       }
-      var height = 0.0;
-      for (var i = 0; i < _months.length; i++) {
-        final item = _months[i];
-        if (item.year == targetDate.year && item.month == targetDate.month) {
-          break;
+      if (!_scrollController.hasClients) {
+        if (retry >= 5) {
+          return;
         }
-        height += _getMonthHeight(_months, i, _monthHeight);
-      }
-      if (height <= 0) {
+        WidgetsBinding.instance.addPostFrameCallback((_) {
+          attemptScroll(retry + 1);
+        });
         return;
       }
+      final position = _scrollController.position;
+      final clamped = height.clamp(
+        position.minScrollExtent,
+        position.maxScrollExtent,
+      );
       if (widget.animateTo) {
         _scrollController.animateTo(
-          height,
+          clamped,
           duration: const Duration(milliseconds: 200),
           curve: Curves.easeInOut,
         );
       } else {
-        _scrollController.jumpTo(height);
+        _scrollController.jumpTo(clamped);
       }
+    }
+
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      attemptScroll();
     });
   }
 

From 2eda3015fec2af676e226a0bedfbef4bfe0d9319 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= 
Date: Wed, 20 May 2026 03:55:20 +0800
Subject: [PATCH 17/35] =?UTF-8?q?perf(calendar):=20=E4=BC=98=E5=8C=96?=
 =?UTF-8?q?=E9=95=BF=E8=B7=9D=E6=BB=9A=E5=8A=A8=E6=80=A7=E8=83=BD=E5=B9=B6?=
 =?UTF-8?q?=E5=B1=8F=E8=94=BD=E4=B8=AD=E9=97=B4=E6=9C=88=E4=BB=BD=E5=9B=9E?=
 =?UTF-8?q?=E8=B0=83?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../example/lib/page/t_calendar_page.dart     |  45 +---
 .../components/calendar/t_calendar_body.dart  | 219 ++++++++++++++----
 2 files changed, 175 insertions(+), 89 deletions(-)

diff --git a/tdesign-component/example/lib/page/t_calendar_page.dart b/tdesign-component/example/lib/page/t_calendar_page.dart
index b4922c1d5..922e6f46f 100644
--- a/tdesign-component/example/lib/page/t_calendar_page.dart
+++ b/tdesign-component/example/lib/page/t_calendar_page.dart
@@ -1,5 +1,3 @@
-import 'dart:async';
-
 import 'package:flutter/material.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import '../../base/example_widget.dart';
@@ -1182,20 +1180,6 @@ class _LunarControlBarState extends State<_LunarControlBar> {
   late DateTime _currentMonth;
   late TCalendarDisplayMode _mode;
 
-  /// 主动跳转期间忽略来自日历 onMonthChange 的中间过渡回调。
-  ///
-  /// 背景:日历内部 animateTo(约 200ms)会沿途触发若干次 onMonthChange,
-  /// 例如从 2026-05 跳到 2023-05,期间会快速地经过 36 个月份;
-  /// 若让控制栏跟随这些中间值刷新,用户视觉上会看到年/月数字"滚屏跳动"。
-  ///
-  /// 因此当用户主动 _navigateTo 时,短暂屏蔽 updateMonth 的中间值,
-  /// 等动画结束后再恢复随手势/上层切换的同步。
-  bool _suppressUpdate = false;
-  Timer? _suppressTimer;
-
-  // 略微大于动画时长(200ms),留出一帧抖动余量。
-  static const Duration _suppressDuration = Duration(milliseconds: 280);
-
   @override
   void initState() {
     super.initState();
@@ -1204,12 +1188,6 @@ class _LunarControlBarState extends State<_LunarControlBar> {
     _mode = TCalendarDisplayMode.solarWithLunar;
   }
 
-  @override
-  void dispose() {
-    _suppressTimer?.cancel();
-    super.dispose();
-  }
-
   /// 将任意 (year, month) clamp 到 [minDate, maxDate] 区间内。
   DateTime _clampMonth(DateTime date) {
     final minKey = widget.minDate.year * 12 + widget.minDate.month;
@@ -1232,12 +1210,12 @@ class _LunarControlBarState extends State<_LunarControlBar> {
     return cur < maxKey;
   }
 
-  /// 由日历 onMonthChange 回调驱动,仅更新显示
+  /// 由日历 onMonthChange 回调驱动,仅更新显示。
+  ///
+  /// 注意:日历组件(TCalendarBody)内部已对程序化滚动期间的 onMonthChange 做静默,
+  /// 因此这里收到的回调只会是「用户手势滑动」或「滚动落定后的最终月份」,
+  /// 无需再做额外的中间值屏蔽。
   void updateMonth(DateTime month) {
-    // 主动跳转动画过程中沿途触发的中间月份不要应用,避免数字跳动。
-    if (_suppressUpdate) {
-      return;
-    }
     if (_currentMonth.year == month.year && _currentMonth.month == month.month) {
       return;
     }
@@ -1251,18 +1229,9 @@ class _LunarControlBarState extends State<_LunarControlBar> {
         _currentMonth.month == clamped.month) {
       return;
     }
-    // 1) 立即更新控制栏显示,用户感知零延迟
+    // 立即更新控制栏显示,用户感知零延迟;
+    // 日历组件会自行屏蔽随后程序化滚动期间的中间月份回调。
     setState(() => _currentMonth = clamped);
-    // 2) 在动画期间屏蔽来自日历的中间月份回调,防止数字跳动
-    _suppressUpdate = true;
-    _suppressTimer?.cancel();
-    _suppressTimer = Timer(_suppressDuration, () {
-      if (!mounted) {
-        return;
-      }
-      _suppressUpdate = false;
-    });
-    // 3) 通知上层触发日历滚动
     widget.onNavigate(DateTime(clamped.year, clamped.month, 15));
   }
 
diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
index 77c4f0701..9f734df47 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
@@ -61,13 +61,26 @@ class TCalendarBody extends StatefulWidget {
 
 class _TCalendarBodyState extends State {
   late final ScrollController _scrollController;
-  DateTime? _lastPrintMonth;
+  int? _lastNotifiedMonthKey;
   final _data = >{};
   final _monthHeight = {};
   late List _months;
   late DateTime _min;
   late DateTime _max;
 
+  /// 月份累计高度的前缀和:`_prefixHeights[i]` = 第 0..i-1 月的高度之和。
+  /// 长度为 `_months.length + 1`,用于 O(log n) 二分查找可见月份。
+  late List _prefixHeights;
+
+  /// 程序化滚动期间静默 onMonthChange 回调,避免高频中间值打扰外部。
+  ///
+  /// 生命周期:
+  /// - 由 `_smoothScrollTo` 在滚动开始时设为 true;
+  /// - 由 `_runAnimateTo.whenComplete` 在动画完成或被打断时复位 false;
+  /// - `addPostFrameCallback` 中检测到 controller 已分离时也会兜底复位,
+  ///   避免外部永久收不到 onMonthChange 回调。
+  bool _programmaticScroll = false;
+
   @override
   void initState() {
     super.initState();
@@ -84,7 +97,7 @@ class _TCalendarBodyState extends State {
         oldWidget.maxDate != widget.maxDate) {
       _monthHeight.clear();
       _data.clear();
-      _lastPrintMonth = null;
+      _lastNotifiedMonthKey = null;
       _initMonths();
     }
     // anchorDate 变化时滚动:使用 identical 引用比较,
@@ -107,6 +120,47 @@ class _TCalendarBodyState extends State {
     _min = _getDefDate(widget.minDate);
     _max = _getDefDate(widget.maxDate, true);
     _months = _monthsBetween(_min, _max);
+    _rebuildIndex();
+  }
+
+  /// 重建月份前缀和。
+  ///
+  /// 性能考量:
+  /// - 此前 `_calcScrollOffset` / `_onScroll` 都是 O(n) 线性扫描,
+  ///   `_months.length` 默认 ~1572 时滚动期间累计开销显著。
+  /// - 改为预计算前缀和 + 二分查找后,单次查询 O(log n),
+  ///   滚动监听不再因列表长度而劣化。
+  ///
+  /// 月份索引不再使用反查 Map:因为 `_months` 按月份单调递增,
+  /// 给定 monthKey 只需 `monthKey - firstKey` 即可 O(1) 算出索引。
+  void _rebuildIndex() {
+    _prefixHeights = List.filled(_months.length + 1, 0.0);
+    var acc = 0.0;
+    for (var i = 0; i < _months.length; i++) {
+      _prefixHeights[i] = acc;
+      acc += _getMonthHeight(_months, i, _monthHeight);
+    }
+    _prefixHeights[_months.length] = acc;
+  }
+
+  static int _monthKey(DateTime d) => d.year * 12 + d.month;
+
+  /// 二分查找:给定滚动偏移量,返回当前可见的月份索引。
+  int _indexAtOffset(double offset) {
+    if (_months.isEmpty) {
+      return 0;
+    }
+    var lo = 0;
+    var hi = _months.length - 1;
+    while (lo < hi) {
+      final mid = (lo + hi + 1) >> 1;
+      if (_prefixHeights[mid] <= offset) {
+        lo = mid;
+      } else {
+        hi = mid - 1;
+      }
+    }
+    return lo;
   }
 
   /// 计算目标日期所在月份的滚动偏移量
@@ -127,62 +181,46 @@ class _TCalendarBodyState extends State {
       return 0.0;
     }
     // 用 (年*12 + 月) 作为可比较的标量,规避日级别比较带来的边界陷阱。
-    final firstKey = _months.first.year * 12 + _months.first.month;
-    final lastKey = _months.last.year * 12 + _months.last.month;
-    final targetKey = scrollToDate.year * 12 + scrollToDate.month;
+    final firstKey = _monthKey(_months.first);
+    final lastKey = _monthKey(_months.last);
+    final targetKey = _monthKey(scrollToDate);
     final clampedKey = targetKey.clamp(firstKey, lastKey);
-    var height = 0.0;
-    for (var i = 0; i < _months.length; i++) {
-      final item = _months[i];
-      final itemKey = item.year * 12 + item.month;
-      if (itemKey == clampedKey) {
-        break;
-      }
-      height += _getMonthHeight(_months, i, _monthHeight);
-    }
-    return height;
+    // O(1) 算术:月份 key 是单调递增的整数,索引 = clampedKey - firstKey。
+    final idx = clampedKey - firstKey;
+    return _prefixHeights[idx];
   }
 
-  int? _lastCleanupIndex;
-
   void _onScroll() {
-    var currentOffset = 0.0;
-    for (var i = 0; i < _months.length; i++) {
-      final mh = _getMonthHeight(_months, i, _monthHeight);
-      if (_scrollController.offset >= currentOffset &&
-          _scrollController.offset < currentOffset + mh) {
-        if (i < _months.length) {
-          final currentMonth = _months[i];
-          if (_lastPrintMonth == null ||
-              !_lastPrintMonth!.isAtSameMomentAs(currentMonth)) {
-            _lastPrintMonth = currentMonth;
-            widget.onMonthChange?.call(currentMonth);
-          }
-        }
-        _cleanupCache(i);
-        break;
+    if (_months.isEmpty) {
+      return;
+    }
+    final i = _indexAtOffset(_scrollController.offset);
+    final currentMonth = _months[i];
+    final currentKey = _monthKey(currentMonth);
+    // 只在月份真正变化时回调,且程序化滚动期间静默,避免动画中间值打扰外部。
+    if (_lastNotifiedMonthKey != currentKey) {
+      _lastNotifiedMonthKey = currentKey;
+      if (!_programmaticScroll) {
+        widget.onMonthChange?.call(currentMonth);
       }
-      currentOffset += mh;
     }
+    _cleanupCache(i);
   }
 
   /// 清理距离当前可见月份过远的缓存数据,避免在 itemBuilder 中执行副作用。
+  ///
+  /// `_data` 实际只会缓存可见月份附近的少量项(受本方法 ±10 范围限制,
+  /// 上限约 21 项),遍历开销可忽略,无需额外节流。
   void _cleanupCache(int currentIndex) {
-    if (_lastCleanupIndex == currentIndex) {
+    if (_months.isEmpty) {
       return;
     }
-    _lastCleanupIndex = currentIndex;
-    final keysToRemove = [];
-    final keyList = [..._data.keys];
-    for (var i = 0; i < keyList.length; i++) {
-      final monthIdx = _months.indexOf(keyList[i]);
-      if (monthIdx < currentIndex - 10 || monthIdx > currentIndex + 10) {
-        keysToRemove.add(keyList[i]);
-      }
-    }
-    for (final key in keysToRemove) {
-      _data.remove(key);
-    }
+    final firstKey = _monthKey(_months.first);
+    _data.removeWhere((key, _) {
+      // 用月份 key 算术替代 List.indexOf 的 O(n) 扫描。
+      final monthIdx = _monthKey(key) - firstKey;
+      return monthIdx < currentIndex - 10 || monthIdx > currentIndex + 10;
+    });
   }
 
   @override
@@ -269,11 +307,7 @@ class _TCalendarBodyState extends State {
         position.maxScrollExtent,
       );
       if (widget.animateTo) {
-        _scrollController.animateTo(
-          clamped,
-          duration: const Duration(milliseconds: 200),
-          curve: Curves.easeInOut,
-        );
+        _smoothScrollTo(position, clamped);
       } else {
         _scrollController.jumpTo(clamped);
       }
@@ -284,6 +318,89 @@ class _TCalendarBodyState extends State {
     });
   }
 
+  /// 长距离滚动优化:先 jumpTo 到目标附近,再做一小段动画补尾。
+  ///
+  /// 背景:`ListView.builder` 在跨度极大(如跨年)时一次性高速滚动会引发:
+  ///   1) itemBuilder 短时间内被调用数十次,每月还要计算农历/节气,主线程被打满
+  ///   2) `_onScroll` 沿途反复触发,进一步加重负载
+  /// 视觉表现就是"卡顿/掉帧"。
+  ///
+  /// 优化策略(参考 iOS 通讯录 / 微信会话列表的"远距离跳转"行为):
+  ///   - 跨度 ≤ 3 屏:保持原有 200ms 平滑动画,体验无变化
+  ///   - 跨度  > 3 屏:先 jumpTo 到距离目标 1 屏的位置(瞬时,无 build 压力),
+  ///                  再用 180ms 平滑滚完最后这 1 屏,仍保留"滑过去"的视觉过渡
+  ///
+  /// 同时把整段滚动标记为「程序化滚动」:期间 `_onScroll` 不向外回调
+  /// onMonthChange,由调用方负责设置目标月份显示,避免中间月份打扰外部状态。
+  void _smoothScrollTo(ScrollPosition position, double target) {
+    final delta = (target - position.pixels).abs();
+    final viewport = position.viewportDimension;
+    // 阈值:超过 3 个屏幕高度就走 jump + animate 的组合方案
+    const thresholdInViewports = 3.0;
+
+    _programmaticScroll = true;
+
+    if (viewport > 0 && delta > viewport * thresholdInViewports) {
+      // 朝目标方向跳到距离目标 1 屏的位置,给最后一段留出动画空间
+      final preJump = target > position.pixels
+          ? target - viewport
+          : target + viewport;
+      _scrollController.jumpTo(preJump.clamp(
+        position.minScrollExtent,
+        position.maxScrollExtent,
+      ));
+      // 关键:jumpTo 后必须等 ListView 完成一次 layout(itemExtentBuilder 重新算
+      // viewport,可见 item 重新生成),否则紧接着 animateTo 可能拿到陈旧的
+      // maxScrollExtent / pixels,触发断言或滚到错误位置。
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        if (!mounted || !_scrollController.hasClients) {
+          _programmaticScroll = false;
+          return;
+        }
+        // 重新 clamp 一次:jumpTo 后 maxScrollExtent 可能因 itemExtentBuilder
+        // 重算而变化(虽然我们用的是固定高度算法,但稳妥起见仍兜底)。
+        final pos = _scrollController.position;
+        final clampedTarget = target.clamp(
+          pos.minScrollExtent,
+          pos.maxScrollExtent,
+        );
+        _runAnimateTo(
+          clampedTarget,
+          const Duration(milliseconds: 180),
+          Curves.easeOut,
+        );
+      });
+    } else {
+      _runAnimateTo(
+        target,
+        const Duration(milliseconds: 200),
+        Curves.easeInOut,
+      );
+    }
+  }
+
+  /// 统一的 animateTo 包装:处理动画完成 / 中断时的状态恢复。
+  void _runAnimateTo(double target, Duration duration, Curve curve) {
+    if (!_scrollController.hasClients) {
+      _programmaticScroll = false;
+      return;
+    }
+    _scrollController
+        .animateTo(target, duration: duration, curve: curve)
+        .whenComplete(() {
+      // State 已 dispose 时直接退出,不再触碰 _scrollController(已 dispose)。
+      if (!mounted) {
+        return;
+      }
+      _programmaticScroll = false;
+      // 落定后补发一次 onMonthChange,让外部确认最终月份;
+      // _onScroll 内部仅在 monthKey 变化时回调,幂等。
+      if (_scrollController.hasClients) {
+        _onScroll();
+      }
+    });
+  }
+
   DateTime _getDefDate(DateTime? date, [bool isMax = false]) {
     if (date != null) {
       return DateTime(date.year, date.month, date.day);

From f2e56dc20f6e5096543cc4fecafa33d437a4a99e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= 
Date: Wed, 20 May 2026 04:20:32 +0800
Subject: [PATCH 18/35] =?UTF-8?q?refactor(calendar):=20=E5=B0=86=E9=80=89?=
 =?UTF-8?q?=E4=B8=AD=E6=80=81=E5=86=B3=E7=AD=96=E9=80=BB=E8=BE=91=E4=BB=8E?=
 =?UTF-8?q?=20cell=20=E4=B8=8A=E7=A7=BB=E8=87=B3=20state?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/components/calendar/t_calendar.dart   | 140 ++++++++++++++++--
 .../components/calendar/t_calendar_body.dart  |  91 +++++++++++-
 .../components/calendar/t_calendar_cell.dart  | 121 ++++-----------
 3 files changed, 247 insertions(+), 105 deletions(-)

diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart
index 2a2205cc4..7bd9a28af 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart
@@ -486,6 +486,19 @@ class _TCalendarState extends State {
 
   List? _cachedValueDates;
 
+  /// single 模式下当前选中的 TDate 引用(来自 body 缓存的当前实例)。
+  ///
+  /// cell 不再反查 `_data` 找上一个 selected:state 维护这条权威引用,点击
+  /// 时直接 setType(empty) 即可。引用会随 body 缓存重生成(cleanup 后再滚回
+  /// 该月)被 [_handleTDateGenerated] 覆盖为新实例,不会出现"指向已 detach
+  /// 的 TDate"导致视觉残留。
+  TDate? _selectedSingleRef;
+
+  /// multiple 模式下当前所有选中的 TDate 引用,按日期键。
+  ///
+  /// 点击切换时直接查表决定 select/empty,避免遍历可见月份缓存。
+  final Map _selectedMultipleRefs = {};
+
   // bottom 展开时日历主体上移的距离,露出 bottom 顶部"把手"区域。
   static const double _bottomPeekHeight = 30.0;
 
@@ -723,15 +736,14 @@ class _TCalendarState extends State {
       onMonthChange: widget.onMonthChange,
       dateType: _dateType,
       dataSource: widget.dataSource,
-      builder: (date, dateList, data, rowIndex, colIndex) {
+      onTDateGenerated: _handleTDateGenerated,
+      onCacheInvalidated: _handleCacheInvalidated,
+      builder: (date, dateList, rowIndex, colIndex) {
         return TCalendarCell(
           height: _getEffectiveCellHeight(),
           tdate: date,
-          type: widget.type,
-          data: data,
           padding: verticalGap / 2,
-          onChange: _handleCellChange,
-          onCellClick: widget.onCellClick,
+          onTap: _handleCellTap,
           dateList: dateList,
           rowIndex: rowIndex,
           colIndex: colIndex,
@@ -743,10 +755,120 @@ class _TCalendarState extends State {
     );
   }
 
-  void _handleCellChange(List value) {
-    final normalized = _getValue(value);
-    inherited?.selected.value = normalized;
-    widget.onChange?.call(normalized);
+  /// 月份 TDate 列表新生成时被 body 调用:登记 selected/start/end 引用,
+  /// 让 state 不依赖 body 内部缓存即可定位"当前选中的那些 TDate 实例"。
+  ///
+  /// single:每月最多一个 selected,遇到即覆盖 _selectedSingleRef。
+  /// multiple:把当月所有 selected 的引用按 date 写入 map。
+  /// range:本身走 widget.value 重建路径,不需要登记。
+  void _handleTDateGenerated(DateTime monthDate, List tdates) {
+    if (widget.type == CalendarType.range) {
+      return;
+    }
+    for (final tdate in tdates) {
+      if (tdate == null) {
+        continue;
+      }
+      if (tdate.typeNotifier.value != DateSelectType.selected) {
+        continue;
+      }
+      if (widget.type == CalendarType.single) {
+        _selectedSingleRef = tdate;
+      } else if (widget.type == CalendarType.multiple) {
+        _selectedMultipleRefs[tdate.date] = tdate;
+      }
+    }
+  }
+
+  /// 当 body 整体清空缓存时(minDate/maxDate 变化等),同步清空选中映射,
+  /// 避免悬挂指向已被替换的 TDate 实例。后续月份重新生成时会再次登记。
+  void _handleCacheInvalidated() {
+    _selectedSingleRef = null;
+    _selectedMultipleRefs.clear();
+  }
+
+  /// 三种模式统一入口:cell 仅上抛被点击的 TDate,由本方法做所有决策。
+  ///
+  /// 行为约定:
+  /// - disabled:仅触发 onCellClick,不改变选中态
+  /// - single:切换 _selectedSingleRef,旧引用置 empty、新引用置 selected
+  /// - multiple:根据 _selectedMultipleRefs 切换该日期的选中态
+  /// - range:交由 [_resolveRangeSelection] 决策后走 setState 重建(保持原有路径)
+  void _handleCellTap(TDate tdate) {
+    final selectType = tdate.typeNotifier.value;
+    final curDate = tdate.date;
+
+    if (selectType == DateSelectType.disabled) {
+      widget.onCellClick?.call(curDate, selectType, tdate);
+      return;
+    }
+
+    switch (widget.type) {
+      case CalendarType.single:
+        // 已经是当前选中:仅触发 onCellClick,不重复 onChange
+        if (identical(_selectedSingleRef, tdate)) {
+          widget.onCellClick?.call(curDate, tdate.typeNotifier.value, tdate);
+          return;
+        }
+        _selectedSingleRef?.typeNotifier.setType(DateSelectType.empty);
+        tdate.typeNotifier.setType(DateSelectType.selected);
+        _selectedSingleRef = tdate;
+        _emitSelection([curDate], rebuild: false);
+        widget.onCellClick?.call(curDate, tdate.typeNotifier.value, tdate);
+        break;
+      case CalendarType.multiple:
+        final existing = _selectedMultipleRefs[curDate];
+        List nextValue;
+        if (existing != null) {
+          existing.typeNotifier.setType(DateSelectType.empty);
+          _selectedMultipleRefs.remove(curDate);
+        } else {
+          tdate.typeNotifier.setType(DateSelectType.selected);
+          _selectedMultipleRefs[curDate] = tdate;
+        }
+        nextValue = _selectedMultipleRefs.keys.toList()..sort();
+        _emitSelection(nextValue, rebuild: false);
+        widget.onCellClick?.call(curDate, tdate.typeNotifier.value, tdate);
+        break;
+      case CalendarType.range:
+        // range 仍走老路径:state 决策 start/end,刷新 value,触发 body 重建。
+        final resolved = _resolveRangeSelection([curDate]);
+        _emitSelection(resolved, rebuild: true);
+        widget.onCellClick?.call(curDate, tdate.typeNotifier.value, tdate);
+        break;
+    }
+  }
+
+  /// 统一更新 _cachedValueDates / inherited.selected / onChange,并按需触发 setState。
+  void _emitSelection(List value, {required bool rebuild}) {
+    _cachedValueDates = value;
+    inherited?.selected.value = value;
+    widget.onChange?.call(value);
+    if (rebuild && mounted) {
+      setState(() {});
+    }
+  }
+
+  /// range 模式专用的选区决策:
+  /// - 无 start:作为新 start
+  /// - 已有 start 且无 end,且点击晚于 start:作为 end,区间完成
+  /// - 其它(已完成区间 / 点击早于等于 start):以本次点击重新开始
+  List _resolveRangeSelection(List rawValue) {
+    if (rawValue.isEmpty) {
+      return const [];
+    }
+    final tapped = DateTime(
+      rawValue.first.year,
+      rawValue.first.month,
+      rawValue.first.day,
+    );
+    final current = _cachedValueDates ?? const [];
+    final hasStart = current.isNotEmpty;
+    final hasEnd = current.length >= 2;
+    if (hasStart && !hasEnd && tapped.isAfter(current[0])) {
+      return [current[0], tapped];
+    }
+    return [tapped];
   }
 
   // 行为约定详见 [TCalendar.popupBottomBuilder]。
diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
index 9f734df47..67b4893f5 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
@@ -25,6 +25,8 @@ class TCalendarBody extends StatefulWidget {
     this.anchorDate,
     this.dateType = TCalendarDateType.solar,
     this.dataSource,
+    this.onTDateGenerated,
+    this.onCacheInvalidated,
   }) : super(key: key);
 
   final DateTime? maxDate;
@@ -36,7 +38,6 @@ class TCalendarBody extends StatefulWidget {
   final Widget Function(
     TDate? date,
     List dateList,
-    Map> data,
     int rowIndex,
     int colIndex,
   ) builder;
@@ -55,6 +56,18 @@ class TCalendarBody extends StatefulWidget {
   final TCalendarDateType dateType;
   final TCalendarDataSource? dataSource;
 
+  /// 在每个月份的 TDate 列表新生成时回调,便于上层把 selected/start/end 等
+  /// 状态的 TDate 引用登记到自己的"权威选中映射"。
+  ///
+  /// 注意:每次 _data 缺失某月时都会生成新 TDate 实例并触发该回调;上层
+  /// 应当以"按需覆盖"语义处理(例如同 date 的旧引用直接被新引用替换)。
+  final void Function(DateTime monthDate, List tdates)?
+      onTDateGenerated;
+
+  /// 当 `_data` 整体被清空(例如 minDate/maxDate 变化或 range 选择变更)时回调,
+  /// 上层据此清空"权威选中映射",避免悬挂指向已被 GC 的旧 TDate 实例。
+  final VoidCallback? onCacheInvalidated;
+
   @override
   State createState() => _TCalendarBodyState();
 }
@@ -88,6 +101,9 @@ class _TCalendarBodyState extends State {
     final initialOffset = _calcScrollOffset();
     _scrollController = ScrollController(initialScrollOffset: initialOffset);
     _scrollController.addListener(_onScroll);
+    // 首屏预热:在第一帧之前生成初始可见月份的数据,避免 itemBuilder
+    // 第一次 build 时缓存为空,回退路径产生不必要的重算。
+    _warmupCacheAround(_indexAtOffset(initialOffset));
   }
 
   @override
@@ -97,8 +113,16 @@ class _TCalendarBodyState extends State {
         oldWidget.maxDate != widget.maxDate) {
       _monthHeight.clear();
       _data.clear();
+      widget.onCacheInvalidated?.call();
       _lastNotifiedMonthKey = null;
       _initMonths();
+    } else if (widget.type == CalendarType.range &&
+        !_listEqualsDate(oldWidget.value, widget.value)) {
+      // range 模式下,state 通过更新 value 来传达选中区间变化。
+      // 必须清空 _data,让所有月份重新基于新 value 推导 cell 类型,
+      // 避免跨月份 cell 的 typeNotifier 残留旧 start/end/centre 状态。
+      _data.clear();
+      widget.onCacheInvalidated?.call();
     }
     // anchorDate 变化时滚动:使用 identical 引用比较,
     // 让上层即使重复传同一年月(例如滑动到该月后再点击该月按钮)也能触发滚动;
@@ -109,6 +133,24 @@ class _TCalendarBodyState extends State {
     }
   }
 
+  static bool _listEqualsDate(List? a, List? b) {
+    if (identical(a, b)) {
+      return true;
+    }
+    if (a == null || b == null) {
+      return false;
+    }
+    if (a.length != b.length) {
+      return false;
+    }
+    for (var i = 0; i < a.length; i++) {
+      if (a[i] != b[i]) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   @override
   void dispose() {
     _scrollController.removeListener(_onScroll);
@@ -204,9 +246,32 @@ class _TCalendarBodyState extends State {
         widget.onMonthChange?.call(currentMonth);
       }
     }
+    _warmupCacheAround(i);
     _cleanupCache(i);
   }
 
+  /// 预热当前可见月份及其前后相邻月份的缓存。
+  ///
+  /// 把"写入 _data"这一副作用从 itemBuilder 中分离出来,避免 build 阶段写状态。
+  /// 范围 ±2 月(共 5 个月)足以覆盖单屏可见与上下少量预渲染月份,超出部分
+  /// 由 itemBuilder 走 fallback 直接计算(仍不写缓存)。
+  void _warmupCacheAround(int currentIndex) {
+    if (_months.isEmpty) {
+      return;
+    }
+    const radius = 2;
+    final lo = (currentIndex - radius).clamp(0, _months.length - 1);
+    final hi = (currentIndex + radius).clamp(0, _months.length - 1);
+    for (var i = lo; i <= hi; i++) {
+      final monthDate = _months[i];
+      if (!_data.containsKey(monthDate)) {
+        final tdates = _getDaysInMonth(monthDate, _min, _max);
+        _data[monthDate] = tdates;
+        widget.onTDateGenerated?.call(monthDate, tdates);
+      }
+    }
+  }
+
   /// 清理距离当前可见月份过远的缓存数据,避免在 itemBuilder 中执行副作用。
   ///
   /// `_data` 实际只会缓存可见月份附近的少量项(受本方法 ±10 范围限制,
@@ -236,12 +301,25 @@ class _TCalendarBodyState extends State {
         final monthYear = monthDate.year.toString() + context.resource.year;
         final monthMonth = widget.monthNames[monthDate.month - 1];
         final monthDateText = '$monthYear $monthMonth';
-        late List monthData;
-        if (_data.containsKey(monthDate)) {
-          monthData = _data[monthDate]!;
+        // 只读:build 不写状态。命中缓存直接用,未命中走纯函数计算并安排
+        // 在下一帧补写缓存(postFrameCallback),避免 build 阶段副作用。
+        List monthData;
+        final cached = _data[monthDate];
+        if (cached != null) {
+          monthData = cached;
         } else {
-          monthData = _data[monthDate] =
-              _getDaysInMonth(monthDate, _min, _max);
+          monthData = _getDaysInMonth(monthDate, _min, _max);
+          WidgetsBinding.instance.addPostFrameCallback((_) {
+            if (!mounted) {
+              return;
+            }
+            // 仅当下一帧仍未被其他路径填充时写入,幂等。
+            // 注册回调也只在真正写入这条新数据时触发,避免重复登记。
+            if (!_data.containsKey(monthDate)) {
+              _data[monthDate] = monthData;
+              widget.onTDateGenerated?.call(monthDate, monthData);
+            }
+          });
         }
 
         return Column(
@@ -267,7 +345,6 @@ class _TCalendarBodyState extends State {
                               ? monthData[rowIndex * 7 + colIndex]
                               : null,
                           monthData,
-                          _data,
                           rowIndex,
                           colIndex,
                         ),
diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart b/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart
index 8b1a92db8..9f68d2e87 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart
@@ -1,17 +1,13 @@
 import 'package:flutter/material.dart';
 import '../../../tdesign_flutter.dart';
 import '../../util/iterable_ext.dart';
-import '../../util/list_ext.dart';
 
 class TCalendarCell extends StatefulWidget {
   const TCalendarCell({
     Key? key,
     this.tdate,
-    required this.type,
-    this.onCellClick,
-    this.onChange,
+    this.onTap,
     required this.height,
-    required this.data,
     required this.padding,
     required this.rowIndex,
     required this.colIndex,
@@ -22,15 +18,14 @@ class TCalendarCell extends StatefulWidget {
   }) : super(key: key);
 
   final TDate? tdate;
-  final CalendarType type;
-  final void Function(
-    DateTime value,
-    DateSelectType selectType,
-    TDate tdate,
-  )? onCellClick;
-  final void Function(List value)? onChange;
+
+  /// 点击回调。cell 不再负责任何选中态决策,只把"被点击的这一格"上抛给
+  /// 上层 state,由其结合 [CalendarType] 决定如何更新选中。
+  ///
+  /// 当点击 disabled cell 时同样会回调(state 内部按需要分流到 onCellClick)。
+  final void Function(TDate tdate)? onTap;
+
   final double height;
-  final Map> data;
   final double padding;
   final int rowIndex;
   final int colIndex;
@@ -76,18 +71,17 @@ class _TCalendarCellState extends State {
 
   @override
   Widget build(BuildContext context) {
-    if (widget.tdate == null) {
+    final tdate = widget.tdate;
+    if (tdate == null) {
       return const SizedBox.shrink();
     }
-    final tdate = widget.tdate!;
-    final cellStyle = TCalendarStyle.cellStyle(context, widget.tdate!._type);
+    final cellStyle = TCalendarStyle.cellStyle(context, tdate._type);
     final decoration = tdate.decoration ?? cellStyle.cellDecoration;
     final positionColor = _getColor(cellStyle, decoration);
 
     // 新增自定义cell内容判断逻辑
-    final content =
-        widget.cellWidget?.call(context, tdate, widget.tdate!._type) ??
-            _buildDefaultCell(context, tdate, cellStyle);
+    final content = widget.cellWidget?.call(context, tdate, tdate._type) ??
+        _buildDefaultCell(context, tdate, cellStyle);
 
     return GestureDetector(
       behavior: HitTestBehavior.translucent,
@@ -118,66 +112,13 @@ class _TCalendarCellState extends State {
   }
 
   void _cellTap() {
-    final list = widget.data.values.expand((element) => element).toList();
-    final selectType = widget.tdate!._type;
-    final curDate = widget.tdate!.date;
-    if (selectType == DateSelectType.disabled) {
-      widget.onCellClick?.call(curDate, selectType, widget.tdate!);
+    final tdate = widget.tdate;
+    if (tdate == null) {
       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?.date != 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!.date).toList();
-        if (date.find((item) => item!.date == 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 startDate = start?.date;
-        if ((start == null && end == null) ||
-            (start != null && end != null) ||
-            (start != null && end == null && !startDate!.isBefore(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 && startDate!.isBefore(curDate)) {
-          start._setType(DateSelectType.start);
-          widget.tdate!._setType(DateSelectType.end);
-          var startIndex = list.indexOf(start) + 1;
-          while (list[startIndex] == null ||
-              list[startIndex]!.date.isBefore(curDate)) {
-            list[startIndex]?._setType(DateSelectType.centre);
-            startIndex++;
-          }
-          widget.onChange?.call([startDate, curDate]);
-        }
-        break;
-    }
-    widget.onCellClick?.call(curDate, widget.tdate!._type, widget.tdate!);
+    // 三种模式统一:cell 不再做任何决策,只把被点击的 TDate 上抛。
+    // 由 [_TCalendarState] 结合 widget.type / 当前选中映射 / value 决定如何更新。
+    widget.onTap?.call(tdate);
   }
 
   void _cellTypeChange() {
@@ -304,6 +245,12 @@ class _TCalendarCellState extends State {
 }
 
 /// 时间对象
+///
+/// 不可变数据载体(除 [typeNotifier] 外所有字段均为 final)。
+///
+/// 选中类型的变化通过 [typeNotifier] 通知监听者;其它视觉字段([prefix] /
+/// [suffix] / [style] / [decoration] 等)请在构造时传入,不要构造后再 mutate
+/// ——这些字段不发出变更通知,cell 也不会响应运行时修改。
 class TDate {
   TDate({
     required this.date,
@@ -330,28 +277,28 @@ class TDate {
   final DateSelectTypeNotifier typeNotifier;
 
   /// 日期前面的字符串
-  String? prefix;
+  final String? prefix;
 
   /// 日期前面的字符串的样式
-  TextStyle? prefixStyle;
+  final TextStyle? prefixStyle;
 
   /// 日期前面的组件,优先级高于[prefix]
-  Widget? prefixWidget;
+  final Widget? prefixWidget;
 
   /// 日期后面的字符串
-  String? suffix;
+  final String? suffix;
 
   /// 日期后面的字符串的样式
-  TextStyle? suffixStyle;
+  final TextStyle? suffixStyle;
 
   /// 日期后面的组件,优先级高于[suffix]
-  Widget? suffixWidget;
+  final Widget? suffixWidget;
 
   /// 日期样式
-  TextStyle? style;
+  final TextStyle? style;
 
   /// 日期Decoration
-  BoxDecoration? decoration;
+  final BoxDecoration? decoration;
 
   /// 是否是当月最后一天
   final bool isLastDayOfMonth;
@@ -371,10 +318,6 @@ class TDate {
   final Map? holidayInfo;
 
   DateSelectType get _type => typeNotifier.value;
-
-  void _setType(DateSelectType type) {
-    typeNotifier.setType(type);
-  }
 }
 
 class DateSelectTypeNotifier extends ChangeNotifier {

From b76a8ae4d87ef18749187cb628d0ad32311e0d1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= 
Date: Wed, 20 May 2026 04:28:12 +0800
Subject: [PATCH 19/35] =?UTF-8?q?test(calendar):=20=E6=9B=B4=E6=96=B0?=
 =?UTF-8?q?=E6=97=A5=E5=8E=86=E7=BB=84=E4=BB=B6=E6=B5=8B=E8=AF=95=E4=BB=A5?=
 =?UTF-8?q?=E9=80=82=E9=85=8DDateTime=E7=B1=BB=E5=9E=8BAPI?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/components/calendar/t_calendar.dart   |   2 +-
 .../test/t_calendar_lunar_test.dart           |  14 +-
 tdesign-component/test/t_calendar_test.dart   | 201 +++++++++---------
 3 files changed, 107 insertions(+), 110 deletions(-)

diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart
index 7bd9a28af..fa340fe74 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart
@@ -7,7 +7,7 @@ import 't_calendar_body.dart';
 import 't_calendar_cell.dart';
 import 't_calendar_header.dart';
 
-export 't_calendar_cell.dart' show TDate;
+export 't_calendar_cell.dart' show TDate, DateSelectTypeNotifier;
 export 't_calendar_data_source.dart';
 export 't_calendar_style.dart';
 export 't_lunar_date.dart';
diff --git a/tdesign-component/test/t_calendar_lunar_test.dart b/tdesign-component/test/t_calendar_lunar_test.dart
index b9607c7b5..0ec34aecc 100644
--- a/tdesign-component/test/t_calendar_lunar_test.dart
+++ b/tdesign-component/test/t_calendar_lunar_test.dart
@@ -4,7 +4,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 void main() {
   group('TLunarInfo', () {
     test('should create lunar info correctly', () {
-      final lunarInfo = TLunarInfo(
+      const lunarInfo = TLunarInfo(
         year: 2025,
         month: 3,
         day: 7,
@@ -24,7 +24,7 @@ void main() {
     });
 
     test('should handle leap month correctly', () {
-      final lunarInfo = TLunarInfo(
+      const lunarInfo = TLunarInfo(
         year: 2025,
         month: 3,
         day: 7,
@@ -40,7 +40,7 @@ void main() {
     });
 
     test('should compare lunar info correctly', () {
-      final info1 = TLunarInfo(
+      const info1 = TLunarInfo(
         year: 2025,
         month: 3,
         day: 7,
@@ -49,7 +49,7 @@ void main() {
         dayText: '初七',
       );
 
-      final info2 = TLunarInfo(
+      const info2 = TLunarInfo(
         year: 2025,
         month: 3,
         day: 7,
@@ -58,7 +58,7 @@ void main() {
         dayText: '初七',
       );
 
-      final info3 = TLunarInfo(
+      const info3 = TLunarInfo(
         year: 2025,
         month: 3,
         day: 8,
@@ -159,7 +159,7 @@ void main() {
 
   group('TDate with LunarInfo', () {
     test('should create TDate with lunar info', () {
-      final lunarInfo = TLunarInfo(
+      const lunarInfo = TLunarInfo(
         year: 2025,
         month: 3,
         day: 7,
@@ -198,7 +198,7 @@ class _MockDataSource extends TCalendarDataSource {
   @override
   TLunarInfo? getLunarInfo(DateTime solarDate) {
     // 简单的 mock 实现
-    return TLunarInfo(
+    return const TLunarInfo(
       year: 2025,
       month: 3,
       day: 7,
diff --git a/tdesign-component/test/t_calendar_test.dart b/tdesign-component/test/t_calendar_test.dart
index 0c22b1cc1..966a0bc44 100644
--- a/tdesign-component/test/t_calendar_test.dart
+++ b/tdesign-component/test/t_calendar_test.dart
@@ -9,22 +9,30 @@ Widget _buildTestApp(Widget child) {
   );
 }
 
+/// 把 [DateTime] 归一化到当天 00:00(与 TCalendar 内部 `_getValue` 行为一致),
+/// 便于在断言中比较。测试 fixture 都使用 `DateTime(y, m, d)` 字面量构造,
+/// 因此默认即为归一化值。
+DateTime _day(int y, int m, int d) => DateTime(y, m, d);
+
 void main() {
-  group('TCalendar — bottom / bottomExpanded', () {
-    test('bottomExpanded 未配合 bottom 时触发 assert', () {
+  // -----------------------------------------------------------------------
+  // popupBottomBuilder / popupBottomExpanded
+  // -----------------------------------------------------------------------
+  group('TCalendar — popupBottom / popupBottomExpanded', () {
+    test('popupBottomExpanded 未配合 popupBottomBuilder 时触发 assert', () {
       expect(
-        () => TCalendar(bottomExpanded: ValueNotifier(false)),
+        () => TCalendar(popupBottomExpanded: ValueNotifier(false)),
         throwsAssertionError,
       );
     });
 
-    testWidgets('非 popup 使用 bottom 触发 assert', (tester) async {
+    testWidgets('非弹窗模式使用 popupBottomBuilder 触发 assert', (tester) async {
       await tester.pumpWidget(
         _buildTestApp(
           TCalendar(
-            title: '测试',
+            titleWidget: const Text('测试'),
             height: 640,
-            bottom: (_, __) => const Text('底部'),
+            popupBottomBuilder: (_, __) => const Text('底部'),
           ),
         ),
       );
@@ -33,11 +41,9 @@ void main() {
       expect(find.text('底部'), findsNothing);
     });
 
-    testWidgets('popup 内选中变化时 bottom 会重建', (tester) async {
-      final day = DateTime(2024, 6, 15);
-      final dayMs =
-          DateTime(day.year, day.month, day.day).millisecondsSinceEpoch;
-      final selected = ValueNotifier>([dayMs]);
+    testWidgets('popup 内选中变化时 popupBottomBuilder 会重建', (tester) async {
+      final day = _day(2024, 6, 15);
+      final selected = ValueNotifier>([day]);
 
       await tester.pumpWidget(
         _buildTestApp(
@@ -45,10 +51,10 @@ void main() {
             selected: selected,
             usePopup: true,
             child: TCalendar(
-              title: '测试',
+              titleWidget: const Text('测试'),
               height: 640,
-              value: selected.value,
-              bottom: (_, dates) => Text('days:${dates.length}'),
+              initialValue: selected.value,
+              popupBottomBuilder: (_, dates) => Text('days:${dates.length}'),
             ),
           ),
         ),
@@ -57,18 +63,15 @@ void main() {
       expect(find.text('days:1'), findsOneWidget);
 
       final day2 = day.add(const Duration(days: 1));
-      selected.value = [
-        dayMs,
-        DateTime(day2.year, day2.month, day2.day).millisecondsSinceEpoch,
-      ];
+      selected.value = [day, _day(day2.year, day2.month, day2.day)];
       await tester.pump();
 
       expect(find.text('days:2'), findsOneWidget);
     });
 
-    testWidgets('popup 内 bottomExpanded 为 false 时处于收起偏移', (tester) async {
+    testWidgets('popup 内 popupBottomExpanded 为 false 时处于收起偏移', (tester) async {
       final expanded = ValueNotifier(false);
-      final selected = ValueNotifier>([]);
+      final selected = ValueNotifier>([]);
 
       await tester.pumpWidget(
         _buildTestApp(
@@ -76,10 +79,10 @@ void main() {
             selected: selected,
             usePopup: true,
             child: TCalendar(
-              title: '测试',
+              titleWidget: const Text('测试'),
               height: 640,
-              bottomExpanded: expanded,
-              bottom: (_, __) => const Text('底部内容'),
+              popupBottomExpanded: expanded,
+              popupBottomBuilder: (_, __) => const Text('底部内容'),
             ),
           ),
         ),
@@ -108,31 +111,25 @@ void main() {
     });
   });
 
-  // -----------------------------------------------------------------------
-  // 辅助:将 DateTime 转为日毫秒时间戳(去除时分秒)
-  // -----------------------------------------------------------------------
-  int _ms(DateTime d) =>
-      DateTime(d.year, d.month, d.day).millisecondsSinceEpoch;
-
   // -----------------------------------------------------------------------
   // 单选
   // -----------------------------------------------------------------------
   group('TCalendar — 单选 (single)', () {
     testWidgets('点击日期触发 onChange', (tester) async {
-      final day15 = DateTime(2024, 6, 15);
-      final day20 = DateTime(2024, 6, 20);
-      final minMs = _ms(DateTime(2024, 6, 1));
-      final maxMs = _ms(DateTime(2024, 6, 30));
-      List? result;
+      final day15 = _day(2024, 6, 15);
+      final day20 = _day(2024, 6, 20);
+      final minDate = _day(2024, 6, 1);
+      final maxDate = _day(2024, 6, 30);
+      List? result;
 
       await tester.pumpWidget(
         _buildTestApp(
           TCalendar(
             height: 640,
             type: CalendarType.single,
-            value: [_ms(day15)],
-            minDate: minMs,
-            maxDate: maxMs,
+            initialValue: [day15],
+            minDate: minDate,
+            maxDate: maxDate,
             onChange: (v) => result = v,
           ),
         ),
@@ -145,13 +142,13 @@ void main() {
 
       expect(result, isNotNull);
       expect(result!.length, 1);
-      expect(result!.first, _ms(day20));
+      expect(result!.first, day20);
     });
 
     testWidgets('点击已选中日期不重复触发 onChange', (tester) async {
-      final day15 = DateTime(2024, 6, 15);
-      final minMs = _ms(DateTime(2024, 6, 1));
-      final maxMs = _ms(DateTime(2024, 6, 30));
+      final day15 = _day(2024, 6, 15);
+      final minDate = _day(2024, 6, 1);
+      final maxDate = _day(2024, 6, 30);
       var callCount = 0;
 
       await tester.pumpWidget(
@@ -159,9 +156,9 @@ void main() {
           TCalendar(
             height: 640,
             type: CalendarType.single,
-            value: [_ms(day15)],
-            minDate: minMs,
-            maxDate: maxMs,
+            initialValue: [day15],
+            minDate: minDate,
+            maxDate: maxDate,
             onChange: (_) => callCount++,
           ),
         ),
@@ -176,19 +173,19 @@ void main() {
     });
 
     testWidgets('点击 disabled 日期不改变选中状态', (tester) async {
-      final day15 = DateTime(2024, 6, 15);
-      final minMs = _ms(DateTime(2024, 6, 10));
-      final maxMs = _ms(DateTime(2024, 6, 25));
-      List? result;
+      final day15 = _day(2024, 6, 15);
+      final minDate = _day(2024, 6, 10);
+      final maxDate = _day(2024, 6, 25);
+      List? result;
 
       await tester.pumpWidget(
         _buildTestApp(
           TCalendar(
             height: 640,
             type: CalendarType.single,
-            value: [_ms(day15)],
-            minDate: minMs,
-            maxDate: maxMs,
+            initialValue: [day15],
+            minDate: minDate,
+            maxDate: maxDate,
             onChange: (v) => result = v,
           ),
         ),
@@ -212,19 +209,19 @@ void main() {
   // -----------------------------------------------------------------------
   group('TCalendar — 多选 (multiple)', () {
     testWidgets('点击新日期添加选中', (tester) async {
-      final day15 = DateTime(2024, 6, 15);
-      final minMs = _ms(DateTime(2024, 6, 1));
-      final maxMs = _ms(DateTime(2024, 6, 30));
-      List? result;
+      final day15 = _day(2024, 6, 15);
+      final minDate = _day(2024, 6, 1);
+      final maxDate = _day(2024, 6, 30);
+      List? result;
 
       await tester.pumpWidget(
         _buildTestApp(
           TCalendar(
             height: 640,
             type: CalendarType.multiple,
-            value: [_ms(day15)],
-            minDate: minMs,
-            maxDate: maxMs,
+            initialValue: [day15],
+            minDate: minDate,
+            maxDate: maxDate,
             onChange: (v) => result = v,
           ),
         ),
@@ -236,25 +233,25 @@ void main() {
 
       expect(result, isNotNull);
       expect(result!.length, 2);
-      expect(result!.contains(_ms(day15)), isTrue);
-      expect(result!.contains(_ms(DateTime(2024, 6, 20))), isTrue);
+      expect(result!.contains(day15), isTrue);
+      expect(result!.contains(_day(2024, 6, 20)), isTrue);
     });
 
     testWidgets('再次点击已选日期取消选中', (tester) async {
-      final day15 = DateTime(2024, 6, 15);
-      final day20 = DateTime(2024, 6, 20);
-      final minMs = _ms(DateTime(2024, 6, 1));
-      final maxMs = _ms(DateTime(2024, 6, 30));
-      List? result;
+      final day15 = _day(2024, 6, 15);
+      final day20 = _day(2024, 6, 20);
+      final minDate = _day(2024, 6, 1);
+      final maxDate = _day(2024, 6, 30);
+      List? result;
 
       await tester.pumpWidget(
         _buildTestApp(
           TCalendar(
             height: 640,
             type: CalendarType.multiple,
-            value: [_ms(day15), _ms(day20)],
-            minDate: minMs,
-            maxDate: maxMs,
+            initialValue: [day15, day20],
+            minDate: minDate,
+            maxDate: maxDate,
             onChange: (v) => result = v,
           ),
         ),
@@ -267,7 +264,7 @@ void main() {
 
       expect(result, isNotNull);
       expect(result!.length, 1);
-      expect(result!.first, _ms(day20));
+      expect(result!.first, day20);
     });
   });
 
@@ -276,17 +273,17 @@ void main() {
   // -----------------------------------------------------------------------
   group('TCalendar — 区间选择 (range)', () {
     testWidgets('选择 start 和 end 触发 onChange', (tester) async {
-      final minMs = _ms(DateTime(2024, 6, 1));
-      final maxMs = _ms(DateTime(2024, 6, 30));
-      List? result;
+      final minDate = _day(2024, 6, 1);
+      final maxDate = _day(2024, 6, 30);
+      List? result;
 
       await tester.pumpWidget(
         _buildTestApp(
           TCalendar(
             height: 640,
             type: CalendarType.range,
-            minDate: minMs,
-            maxDate: maxMs,
+            minDate: minDate,
+            maxDate: maxDate,
             onChange: (v) => result = v,
           ),
         ),
@@ -299,31 +296,31 @@ void main() {
 
       expect(result, isNotNull);
       expect(result!.length, 1);
-      expect(result!.first, _ms(DateTime(2024, 6, 10)));
+      expect(result!.first, _day(2024, 6, 10));
 
       // 再点 20 号作为 end
       await tester.tap(find.text('20'));
       await tester.pump();
 
       expect(result!.length, 2);
-      expect(result!.first, _ms(DateTime(2024, 6, 10)));
-      expect(result!.last, _ms(DateTime(2024, 6, 20)));
+      expect(result!.first, _day(2024, 6, 10));
+      expect(result!.last, _day(2024, 6, 20));
     });
 
     testWidgets('end 在 start 之前时重置为新 start', (tester) async {
-      final day20 = DateTime(2024, 6, 20);
-      final minMs = _ms(DateTime(2024, 6, 1));
-      final maxMs = _ms(DateTime(2024, 6, 30));
-      List? result;
+      final day20 = _day(2024, 6, 20);
+      final minDate = _day(2024, 6, 1);
+      final maxDate = _day(2024, 6, 30);
+      List? result;
 
       await tester.pumpWidget(
         _buildTestApp(
           TCalendar(
             height: 640,
             type: CalendarType.range,
-            value: [_ms(day20)],
-            minDate: minMs,
-            maxDate: maxMs,
+            initialValue: [day20],
+            minDate: minDate,
+            maxDate: maxDate,
             onChange: (v) => result = v,
           ),
         ),
@@ -336,7 +333,7 @@ void main() {
 
       expect(result, isNotNull);
       expect(result!.length, 1);
-      expect(result!.first, _ms(DateTime(2024, 6, 10)));
+      expect(result!.first, _day(2024, 6, 10));
     });
   });
 
@@ -344,14 +341,14 @@ void main() {
   // 边界条件
   // -----------------------------------------------------------------------
   group('TCalendar — 边界条件', () {
-    testWidgets('不传 value 时正常渲染', (tester) async {
+    testWidgets('不传 initialValue 时正常渲染', (tester) async {
       await tester.pumpWidget(
         _buildTestApp(
           TCalendar(
             height: 640,
             type: CalendarType.single,
-            minDate: _ms(DateTime(2024, 6, 1)),
-            maxDate: _ms(DateTime(2024, 6, 30)),
+            minDate: _day(2024, 6, 1),
+            maxDate: _day(2024, 6, 30),
           ),
         ),
       );
@@ -362,15 +359,15 @@ void main() {
       expect(find.text('30'), findsOneWidget);
     });
 
-    testWidgets('空 value 列表正常渲染', (tester) async {
+    testWidgets('空 initialValue 列表正常渲染', (tester) async {
       await tester.pumpWidget(
         _buildTestApp(
           TCalendar(
             height: 640,
             type: CalendarType.single,
-            value: const [],
-            minDate: _ms(DateTime(2024, 6, 1)),
-            maxDate: _ms(DateTime(2024, 6, 30)),
+            initialValue: const [],
+            minDate: _day(2024, 6, 1),
+            maxDate: _day(2024, 6, 30),
           ),
         ),
       );
@@ -380,9 +377,9 @@ void main() {
     });
 
     testWidgets('onCellClick 回调携带正确参数', (tester) async {
-      final minMs = _ms(DateTime(2024, 6, 1));
-      final maxMs = _ms(DateTime(2024, 6, 30));
-      int? clickedValue;
+      final minDate = _day(2024, 6, 1);
+      final maxDate = _day(2024, 6, 30);
+      DateTime? clickedValue;
       DateSelectType? clickedType;
 
       await tester.pumpWidget(
@@ -390,8 +387,8 @@ void main() {
           TCalendar(
             height: 640,
             type: CalendarType.single,
-            minDate: minMs,
-            maxDate: maxMs,
+            minDate: minDate,
+            maxDate: maxDate,
             onCellClick: (value, type, tdate) {
               clickedValue = value;
               clickedType = type;
@@ -404,7 +401,7 @@ void main() {
       await tester.tap(find.text('15'));
       await tester.pump();
 
-      expect(clickedValue, _ms(DateTime(2024, 6, 15)));
+      expect(clickedValue, _day(2024, 6, 15));
       expect(clickedType, DateSelectType.selected);
     });
 
@@ -414,8 +411,8 @@ void main() {
           TCalendar(
             height: 640,
             type: CalendarType.single,
-            minDate: _ms(DateTime(2024, 6, 1)),
-            maxDate: _ms(DateTime(2024, 6, 1)),
+            minDate: _day(2024, 6, 1),
+            maxDate: _day(2024, 6, 1),
           ),
         ),
       );
@@ -432,8 +429,8 @@ void main() {
             height: 640,
             type: CalendarType.single,
             firstDayOfWeek: 1,
-            minDate: _ms(DateTime(2024, 6, 1)),
-            maxDate: _ms(DateTime(2024, 6, 30)),
+            minDate: _day(2024, 6, 1),
+            maxDate: _day(2024, 6, 30),
           ),
         ),
       );

From 929bd9b026395f50f8ce44aba29db53c9a2f8675 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= 
Date: Wed, 20 May 2026 04:49:13 +0800
Subject: [PATCH 20/35] =?UTF-8?q?fix(calendar):=20=E4=BF=AE=E6=AD=A3?=
 =?UTF-8?q?=E8=8C=83=E5=9B=B4=E9=80=89=E6=8B=A9=E7=82=B9=E5=87=BB=E6=97=B6?=
 =?UTF-8?q?=E4=B8=8A=E6=8A=A5=E7=9A=84=E8=AF=AD=E4=B9=89=E7=B1=BB=E5=9E=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../lib/src/components/calendar/t_calendar.dart           | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart
index fa340fe74..d0155c57c 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart
@@ -834,7 +834,13 @@ class _TCalendarState extends State {
         // range 仍走老路径:state 决策 start/end,刷新 value,触发 body 重建。
         final resolved = _resolveRangeSelection([curDate]);
         _emitSelection(resolved, rebuild: true);
-        widget.onCellClick?.call(curDate, tdate.typeNotifier.value, tdate);
+        // 上抛点击时已根据 resolved 推导出本次的语义类型(start / end),
+        // 这样调用方无需等到 body 重建后再读 typeNotifier,可直接用于
+        // 切换关联 UI(如时间选择器 Tab)。
+        final reportedType = resolved.length >= 2 && resolved[1] == curDate
+            ? DateSelectType.end
+            : DateSelectType.start;
+        widget.onCellClick?.call(curDate, reportedType, tdate);
         break;
     }
   }

From 3538406b1b366d298dfb5f0a934372787b74c8e4 Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Tue, 19 May 2026 21:03:24 +0000
Subject: [PATCH 21/35] [autofix.ci] apply automated fixes

---
 .../example/assets/api/calendar_api.md        |  47 +-
 .../assets/code/calendar._buildLunar.txt      | 300 +-----
 tdesign-site/src/calendar/README.md           | 915 +++---------------
 3 files changed, 143 insertions(+), 1119 deletions(-)

diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md
index 77aba4638..714f5abd1 100644
--- a/tdesign-component/example/assets/api/calendar_api.md
+++ b/tdesign-component/example/assets/api/calendar_api.md
@@ -4,43 +4,36 @@
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| anchorDate | DateTime? | - | 锚点日期 |
-| animateTo | bool? | false | 动画滚动到指定位置 |
-| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,以浮层方式叠加在日历主体之上。 |
-| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。**仅能在 [TPopupBottomDisplayPanel] 内使用。** |
-| cellHeight | double? | 60 | 日期高度 |
+| anchorDate | DateTime? | - | 锚点日期,弹出时自动滚动到该日期所在月份。 |
+| animateTo | bool | false | 滚动到选中日期/锚点日期所在月份时是否使用动画,默认 false |
+| cellHeight | double? | - | 日期单元格高度,默认 60。如需更大行高可传入自定义值(如 80) |
 | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 |
-| dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 |
-| dateType | TCalendarDateType | TCalendarDateType.solar | 日历类型:阳历或农历 |
-| displayFormat | String? | 'year month' | 年月显示格式,`year`表示年,`month`表示月,如`year month`表示年在前、月在后、中间隔一个空格 |
-| firstDayOfWeek | int? | 0 | 第一天从星期几开始,默认 0 = 周日 |
-| format | CalendarFormat? | - | 用于格式化日期的函数,可定义日期前后的显示内容和日期样式 |
+| dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能。 |
+| displayMode | TCalendarDisplayMode | TCalendarDisplayMode.solar | 日历显示模式,控制日期单元格的主/副文本内容: |
+| firstDayOfWeek | int | 0 | 第一天从星期几开始,0 = 周日,1 = 周一,…,6 = 周六。默认 0(周日)。 |
 | height | double? | - | 高度,不传时内嵌模式自动按 5 行日期计算 |
+| initialValue | List? | - | 初始选中日期列表,不传则默认今天。 |
 | key |  | - |  |
-| maxDate | int? | - | 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认 2100-12-31 |
-| minDate | int? | - | 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认 1970-01-01 |
+| maxDate | DateTime? | - | 最大可选的日期,不传则默认 2100-12-31 |
+| minDate | DateTime? | - | 最小可选的日期,不传则默认 1970-01-01 |
 | monthTitleBuilder | Widget Function(BuildContext context, DateTime monthDate)? | - | 月标题构建器 |
-| monthTitleHeight | double? | 22 | 月标题高度 |
-| onCellClick | void Function(int value, DateSelectType type, TDate tdate)? | - | 点击日期时触发 |
-| onCellLongPress | void Function(int value, DateSelectType type, TDate tdate)? | - | 长按日期时触发 |
-| onChange | void Function(List value)? | - | 选中值变化时触发 |
-| onHeaderClick | void Function(int index, String week)? | - | 点击周时触发 |
+| monthTitleHeight | double | 22 | 每月标题行高度(如 '2025年6月' 所在行),默认 22 |
+| onCellClick | void Function(DateTime value, DateSelectType selectType, TDate tdate)? | - | 点击日期时触发 |
+| onChange | void Function(List value)? | - | 选中值变化时触发 |
 | onMonthChange | ValueChanged? | - | 月份变化时触发 |
-| showLunarInfo | bool | false | 阳历模式下是否显示农历信息作为副标题 |
+| popupBottomBuilder | Widget Function(BuildContext context, List selectedDates)? | - | 弹窗底部自定义区域构建器,以浮层方式叠加在日历主体之上。 |
+| popupBottomExpanded | ValueListenable? | - | 弹窗底部区域是否展开(响应式)。**仅在弹窗模式下生效。** |
+| safeAreaInset | bool | true | 是否适配底部安全区域(如 iPhone Home Indicator),默认 true |
 | style | TCalendarStyle? | - | 自定义样式 |
-| title | String? | - | 标题 |
-| titleWidget | Widget? | - | 标题组件 |
-| type | CalendarType? | CalendarType.single | 日历的选择类型,single = 单选;multiple = 多选;range = 区间选择 |
-| useSafeArea | bool? | true | 是否使用安全区域(默认 true) |
-| value | List? | - | 当前选择的日期(fromMillisecondsSinceEpoch),不传则默认今天,当 type = single 时数组长度为1 |
-| width | double? | - | 宽度 |
+| titleWidget | Widget? | - | 标题组件,可传入 Text 或自定义 Widget |
+| type | CalendarType | CalendarType.single | 日历的选择模式,决定点击日期后的选中行为: |
 
 
 #### 静态方法
 
 | 名称 | 返回类型 | 参数 | 说明 |
 | --- | --- | --- | --- |
-| showPopup |  |   required BuildContext context,  String? title,  CalendarType type,  List? value,  int? minDate,  int? maxDate,  DateTime? anchorDate,  double? fixedHeight,  int? firstDayOfWeek,  String? displayFormat,  double? cellHeight,  TCalendarStyle? style,  CalendarFormat? format,  CalendarBottomBuilder? bottom,  ValueListenable? bottomExpanded,  Widget? confirmBtn,  void Function(List)? onConfirm,  VoidCallback? onClose,  void Function(int value, DateSelectType type, TDate tdate)? onCellClick,  void Function(int value, DateSelectType type, TDate tdate)? onCellLongPress,  bool autoClose,  bool draggable,  Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? cellWidget,  TCalendarDateType dateType,  TCalendarDataSource? dataSource,  bool showLunarInfo,  ValueChanged? onMonthChange,  Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。     取消或关闭弹窗时返回 `null`;点击确认时返回选中日期的毫秒时间戳列表。     ```dart   final result = await TCalendar.showPopup(     context,     title: '请选择日期',     type: CalendarType.single,   );   if (result != null) {     print('选中了: $result');   }   ```     若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel]   + [TSlidePopupRoute] 自行组装。 |
+| showPopup |  |   required BuildContext context,  Widget? titleWidget,  CalendarType type,  List? initialValue,  DateTime? minDate,  DateTime? maxDate,  DateTime? anchorDate,  double? popupHeight,  int firstDayOfWeek,  double? cellHeight,  TCalendarStyle? style,  Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder,  ValueListenable? popupBottomExpanded,  Widget? confirmBtn,  void Function(List)? onConfirm,  VoidCallback? onClose,  void Function(DateTime value, DateSelectType selectType, TDate tdate)? onCellClick,  bool autoClose,  bool draggable,  Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? cellWidget,  TCalendarDisplayMode displayMode,  TCalendarDataSource? dataSource,  ValueChanged? onMonthChange,  Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。     取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。     ```dart   final result = await TCalendar.showPopup(     context,     titleWidget: Text('请选择日期'),     type: CalendarType.single,   );   if (result != null) {     print('选中了: $result');   }   ```     若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel]   + [TSlidePopupRoute] 自行组装。 |
 
 ```
 ```
@@ -58,8 +51,8 @@
 | decoration |  | - |  |
 | monthTitleStyle | TextStyle? | - | body区域 年月文字样式 |
 | titleCloseColor | Color? | - | header区域 关闭图标的颜色 |
-| titleMaxLine | int? | - | header区域 [TCalendar.title]的行数 |
-| titleStyle | TextStyle? | - | header区域 [TCalendar.title]的样式 |
+| titleMaxLine | int? | - | header区域 [TCalendar.titleWidget]的行数 |
+| titleStyle | TextStyle? | - | header区域 [TCalendar.titleWidget]的样式 |
 | todayStyle | TextStyle? | - | 当天日期样式 |
 | weekdayStyle | TextStyle? | - | header区域 周 文字样式 |
 
diff --git a/tdesign-component/example/assets/code/calendar._buildLunar.txt b/tdesign-component/example/assets/code/calendar._buildLunar.txt
index 0eef4e127..16a41912b 100644
--- a/tdesign-component/example/assets/code/calendar._buildLunar.txt
+++ b/tdesign-component/example/assets/code/calendar._buildLunar.txt
@@ -1,302 +1,4 @@
 
 Widget _buildLunar(BuildContext context) {
-  final dataSource = LunarDataSourceExample();
-  
-  // 当前月份状态
-  final currentMonth = ValueNotifier(
-    DateTime(DateTime.now().year, DateTime.now().month, 1),
-  );
-  
-  // 农历开关状态
-  final showLunarInfo = ValueNotifier(true);
-  
-  // 选中日期
-  final selectedDate = ValueNotifier>([
-    DateTime.now().millisecondsSinceEpoch,
-  ]);
-
-  return Column(
-    crossAxisAlignment: CrossAxisAlignment.start,
-    children: [
-      // 控制栏
-      Container(
-        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
-        decoration: BoxDecoration(
-          color: Colors.grey.shade50,
-          border: Border(
-            bottom: BorderSide(color: Colors.grey.shade200),
-          ),
-        ),
-        child: ValueListenableBuilder(
-          valueListenable: currentMonth,
-          builder: (context, month, child) {
-            // 获取当前月份的农历信息
-            final lunarInfo = dataSource.getLunarInfo(month);
-            final lunarMonth = lunarInfo != null 
-                ? '${lunarInfo.yearText}年 ${lunarInfo.monthText}' 
-                : '';
-
-            return Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                // 农历年月显示
-                if (lunarMonth.isNotEmpty)
-                  Padding(
-                    padding: const EdgeInsets.only(bottom: 8),
-                    child: Text(
-                      lunarMonth,
-                      style: TextStyle(
-                        fontSize: 14,
-                        color: Colors.grey.shade700,
-                        fontWeight: FontWeight.w500,
-                      ),
-                    ),
-                  ),
-                // 按钮行
-                Row(
-                  children: [
-                    // 上一月按钮
-                    TButton(
-                      text: '上一月',
-                      size: TButtonSize.small,
-                      theme: TButtonTheme.primary,
-                      onTap: () {
-                        currentMonth.value = DateTime(
-                          month.year,
-                          month.month - 1,
-                          1,
-                        );
-                        selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                      },
-                    ),
-                    const SizedBox(width: 8),
-                    // 年份选择
-                    Expanded(
-                      child: TButton(
-                        text: '${month.year}年',
-                        size: TButtonSize.small,
-                        theme: TButtonTheme.defaultTheme,
-                        onTap: () async {
-                          final year = await showModalBottomSheet(
-                            context: context,
-                            builder: (context) {
-                              return SizedBox(
-                                height: 300,
-                                child: Column(
-                                  children: [
-                                    const Padding(
-                                      padding: EdgeInsets.all(16),
-                                      child: Text(
-                                        '选择年份',
-                                        style: TextStyle(
-                                          fontSize: 18,
-                                          fontWeight: FontWeight.bold,
-                                        ),
-                                      ),
-                                    ),
-                                    Expanded(
-                                      child: ListView.builder(
-                                        itemCount: 50,
-                                        itemBuilder: (context, index) {
-                                          final year = DateTime.now().year - 10 + index;
-                                          final isSelected = year == month.year;
-                                          return ListTile(
-                                            title: Text(
-                                              '$year年',
-                                              style: TextStyle(
-                                                color: isSelected ? Colors.blue : null,
-                                                fontWeight: isSelected ? FontWeight.bold : null,
-                                              ),
-                                            ),
-                                            onTap: () => Navigator.pop(context, year),
-                                          );
-                                        },
-                                      ),
-                                    ),
-                                  ],
-                                ),
-                              );
-                            },
-                          );
-                          if (year != null) {
-                            currentMonth.value = DateTime(year, month.month, 1);
-                            selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                          }
-                        },
-                      ),
-                    ),
-                    const SizedBox(width: 8),
-                    // 月份选择
-                    Expanded(
-                      child: TButton(
-                        text: '${month.month}月',
-                        size: TButtonSize.small,
-                        theme: TButtonTheme.defaultTheme,
-                        onTap: () async {
-                          final selectedMonth = await showModalBottomSheet(
-                            context: context,
-                            builder: (context) {
-                              return SizedBox(
-                                height: 400,
-                                child: Column(
-                                  children: [
-                                    const Padding(
-                                      padding: EdgeInsets.all(16),
-                                      child: Text(
-                                        '选择月份',
-                                        style: TextStyle(
-                                          fontSize: 18,
-                                          fontWeight: FontWeight.bold,
-                                        ),
-                                      ),
-                                    ),
-                                    Expanded(
-                                      child: GridView.builder(
-                                        padding: const EdgeInsets.all(16),
-                                        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
-                                          crossAxisCount: 3,
-                                          childAspectRatio: 2,
-                                          crossAxisSpacing: 10,
-                                          mainAxisSpacing: 10,
-                                        ),
-                                        itemCount: 12,
-                                        itemBuilder: (context, index) {
-                                          final m = index + 1;
-                                          final isSelected = m == month.month;
-                                          return InkWell(
-                                            onTap: () => Navigator.pop(context, m),
-                                            child: Container(
-                                              alignment: Alignment.center,
-                                              decoration: BoxDecoration(
-                                                color: isSelected ? Colors.blue : Colors.grey.shade200,
-                                                borderRadius: BorderRadius.circular(8),
-                                              ),
-                                              child: Text(
-                                                '$m月',
-                                                style: TextStyle(
-                                                  color: isSelected ? Colors.white : Colors.black,
-                                                  fontWeight: isSelected ? FontWeight.bold : null,
-                                                ),
-                                              ),
-                                            ),
-                                          );
-                                        },
-                                      ),
-                                    ),
-                                  ],
-                                ),
-                              );
-                            },
-                          );
-                          if (selectedMonth != null) {
-                            currentMonth.value = DateTime(month.year, selectedMonth, 1);
-                            selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                          }
-                        },
-                      ),
-                    ),
-                    const SizedBox(width: 8),
-                    // 下一月按钮
-                    TButton(
-                      text: '下一月',
-                      size: TButtonSize.small,
-                      theme: TButtonTheme.primary,
-                      onTap: () {
-                        currentMonth.value = DateTime(
-                          month.year,
-                          month.month + 1,
-                          1,
-                        );
-                        selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                      },
-                    ),
-                    const SizedBox(width: 16),
-                    // 农历开关
-                    ValueListenableBuilder(
-                      valueListenable: showLunarInfo,
-                      builder: (context, show, child) {
-                        return Row(
-                          mainAxisSize: MainAxisSize.min,
-                          children: [
-                            Text(
-                              '农历',
-                              style: TextStyle(fontSize: 12, color: Colors.grey.shade700),
-                            ),
-                            Switch(
-                              value: show,
-                              onChanged: (value) {
-                                showLunarInfo.value = value;
-                              },
-                              materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
-                            ),
-                          ],
-                        );
-                      },
-                    ),
-                  ],
-                ),
-              ],
-            );
-          },
-        ),
-      ),
-      const SizedBox(height: 16),
-      // 日历主体
-      ValueListenableBuilder(
-        valueListenable: showLunarInfo,
-        builder: (context, show, child) {
-          return ValueListenableBuilder(
-            valueListenable: selectedDate,
-            builder: (context, value, child) {
-              return TCalendar(
-                title: '',
-                showLunarInfo: show,
-                dataSource: dataSource,
-                value: value,
-                onChange: (newValue) {
-                  selectedDate.value = newValue;
-                  
-                  // 显示完整农历信息
-                  final date = DateTime.fromMillisecondsSinceEpoch(newValue[0]);
-                  final lunarInfo = dataSource.getLunarInfo(date);
-                  final solarTerm = dataSource.getSolarTerm(date);
-                  final festival = dataSource.getFestival(date, lunarInfo);
-                  final holidayInfo = dataSource.getHolidayInfo(date);
-                  
-                  final buffer = StringBuffer();
-                  buffer.write('阳历:${date.year}年${date.month}月${date.day}日');
-                  
-                  if (lunarInfo != null) {
-                    buffer.write('\n农历:${lunarInfo.monthText}${lunarInfo.dayText}');
-                  }
-                  
-                  if (solarTerm != null && solarTerm.isNotEmpty) {
-                    buffer.write('\n节气:$solarTerm');
-                  }
-                  
-                  if (festival != null && festival.isNotEmpty) {
-                    buffer.write('\n节日:$festival');
-                  }
-                  
-                  if (holidayInfo != null) {
-                    final type = holidayInfo['type'] == 'holiday' ? '假期' : '调休';
-                    buffer.write('\n$type:${holidayInfo['name']}');
-                  }
-                  
-                  ScaffoldMessenger.of(context).clearSnackBars();
-                  ScaffoldMessenger.of(context).showSnackBar(
-                    SnackBar(
-                      content: Text(buffer.toString()),
-                      duration: const Duration(seconds: 3),
-                      behavior: SnackBarBehavior.floating,
-                    ),
-                  );
-                },
-              );
-            },
-          );
-        },
-      ),
-    ],
-  );
+  return const _LunarCalendarDemo();
 }
\ No newline at end of file
diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md
index d6eb71b8a..827428f5a 100644
--- a/tdesign-site/src/calendar/README.md
+++ b/tdesign-site/src/calendar/README.md
@@ -49,45 +49,62 @@ Widget _buildStyle(BuildContext context) {
     15: '元宵节',
   };
 
-  final customCellSelected = ValueNotifier>(
-      [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]);
+  final customCellSelected = ValueNotifier>(
+      [DateTime.now().add(const Duration(days: 30))]);
 
   return ValueListenableBuilder(
     valueListenable: customCellSelected,
     builder: (context, cellValue, _) {
-      final cellDate = DateTime.fromMillisecondsSinceEpoch(cellValue[0]);
+      final cellDate = cellValue[0];
       return TCellGroup(
         cells: [
-          // 1. 自定义文案(format 回调修改 prefix/suffix/style)
+          // 1. 自定义文案(cellWidget 回调自定义 cell 渲染)
           TCell(
             title: '自定义文案',
             arrow: true,
             onClick: (cell) {
               TCalendar.showPopup(
                 context,
-                title: '请选择日期',
-                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: TTheme.of(context).fontTitleMedium?.size,
-                        height: TTheme.of(context).fontTitleMedium?.height,
-                        fontWeight:
-                            TTheme.of(context).fontTitleMedium?.fontWeight,
-                        color: TTheme.of(context).errorColor6,
-                      );
-                      if (day?.typeNotifier.value == DateSelectType.selected) {
-                        day?.style = day.style
-                            ?.copyWith(color: TTheme.of(context).fontWhColor1);
-                      }
-                    }
-                  }
-                  return null;
+                titleWidget: const Text('请选择日期'),
+                minDate: DateTime(2022, 1, 1),
+                maxDate: DateTime(2022, 2, 15),
+                cellWidget: (context, tdate, selectType) {
+                  final isSpecial = tdate.date.month == 2 &&
+                      map.keys.contains(tdate.date.day);
+                  final suffix = isSpecial ? '¥100' : '¥60';
+                  final prefix = isSpecial ? map[tdate.date.day] : null;
+                  return Column(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      if (prefix != null)
+                        Text(prefix,
+                            style: TextStyle(
+                              fontSize: 9,
+                              color: isSpecial
+                                  ? TTheme.of(context).errorColor6
+                                  : null,
+                            )),
+                      Text(
+                        tdate.date.day.toString(),
+                        style: TextStyle(
+                          color: selectType == DateSelectType.selected
+                              ? TTheme.of(context).fontWhColor1
+                              : isSpecial
+                                  ? TTheme.of(context).errorColor6
+                                  : null,
+                        ),
+                      ),
+                      Text(suffix,
+                          style: TextStyle(
+                            fontSize: 9,
+                            color: selectType == DateSelectType.selected
+                                ? TTheme.of(context).fontWhColor1
+                                : isSpecial
+                                    ? TTheme.of(context).errorColor6
+                                    : null,
+                          )),
+                    ],
+                  );
                 },
               );
             },
@@ -100,8 +117,8 @@ Widget _buildStyle(BuildContext context) {
             onClick: (cell) {
               TCalendar.showPopup(
                 context,
-                title: '请选择日期',
-                value: [DateTime.now().millisecondsSinceEpoch],
+                titleWidget: const Text('请选择日期'),
+                initialValue: [DateTime.now()],
                 confirmBtn: Padding(
                   padding: EdgeInsets.symmetric(
                       vertical: TTheme.of(context).spacer16),
@@ -126,15 +143,14 @@ Widget _buildStyle(BuildContext context) {
             onClick: (cell) {
               TCalendar.showPopup(
                 context,
-                title: '请选择日期',
-                value: cellValue,
+                titleWidget: const Text('请选择日期'),
+                initialValue: cellValue,
                 cellHeight: 80,
                 onConfirm: (value) => customCellSelected.value = value,
                 cellWidget: (context, tdate, selectType) {
                   final today = DateTime.now();
-                  final isToday = tdate.date.millisecondsSinceEpoch ==
-                      DateTime(today.year, today.month, today.day)
-                          .millisecondsSinceEpoch;
+                  final isToday = tdate.date ==
+                      DateTime(today.year, today.month, today.day);
 
                   if (isToday && selectType != DateSelectType.selected) {
                     return _CustomCellContainer(
@@ -199,45 +215,62 @@ Widget _buildStyle(BuildContext context) {
     15: '元宵节',
   };
 
-  final customCellSelected = ValueNotifier>(
-      [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000]);
+  final customCellSelected = ValueNotifier>(
+      [DateTime.now().add(const Duration(days: 30))]);
 
   return ValueListenableBuilder(
     valueListenable: customCellSelected,
     builder: (context, cellValue, _) {
-      final cellDate = DateTime.fromMillisecondsSinceEpoch(cellValue[0]);
+      final cellDate = cellValue[0];
       return TCellGroup(
         cells: [
-          // 1. 自定义文案(format 回调修改 prefix/suffix/style)
+          // 1. 自定义文案(cellWidget 回调自定义 cell 渲染)
           TCell(
             title: '自定义文案',
             arrow: true,
             onClick: (cell) {
               TCalendar.showPopup(
                 context,
-                title: '请选择日期',
-                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: TTheme.of(context).fontTitleMedium?.size,
-                        height: TTheme.of(context).fontTitleMedium?.height,
-                        fontWeight:
-                            TTheme.of(context).fontTitleMedium?.fontWeight,
-                        color: TTheme.of(context).errorColor6,
-                      );
-                      if (day?.typeNotifier.value == DateSelectType.selected) {
-                        day?.style = day.style
-                            ?.copyWith(color: TTheme.of(context).fontWhColor1);
-                      }
-                    }
-                  }
-                  return null;
+                titleWidget: const Text('请选择日期'),
+                minDate: DateTime(2022, 1, 1),
+                maxDate: DateTime(2022, 2, 15),
+                cellWidget: (context, tdate, selectType) {
+                  final isSpecial = tdate.date.month == 2 &&
+                      map.keys.contains(tdate.date.day);
+                  final suffix = isSpecial ? '¥100' : '¥60';
+                  final prefix = isSpecial ? map[tdate.date.day] : null;
+                  return Column(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      if (prefix != null)
+                        Text(prefix,
+                            style: TextStyle(
+                              fontSize: 9,
+                              color: isSpecial
+                                  ? TTheme.of(context).errorColor6
+                                  : null,
+                            )),
+                      Text(
+                        tdate.date.day.toString(),
+                        style: TextStyle(
+                          color: selectType == DateSelectType.selected
+                              ? TTheme.of(context).fontWhColor1
+                              : isSpecial
+                                  ? TTheme.of(context).errorColor6
+                                  : null,
+                        ),
+                      ),
+                      Text(suffix,
+                          style: TextStyle(
+                            fontSize: 9,
+                            color: selectType == DateSelectType.selected
+                                ? TTheme.of(context).fontWhColor1
+                                : isSpecial
+                                    ? TTheme.of(context).errorColor6
+                                    : null,
+                          )),
+                    ],
+                  );
                 },
               );
             },
@@ -250,8 +283,8 @@ Widget _buildStyle(BuildContext context) {
             onClick: (cell) {
               TCalendar.showPopup(
                 context,
-                title: '请选择日期',
-                value: [DateTime.now().millisecondsSinceEpoch],
+                titleWidget: const Text('请选择日期'),
+                initialValue: [DateTime.now()],
                 confirmBtn: Padding(
                   padding: EdgeInsets.symmetric(
                       vertical: TTheme.of(context).spacer16),
@@ -276,15 +309,14 @@ Widget _buildStyle(BuildContext context) {
             onClick: (cell) {
               TCalendar.showPopup(
                 context,
-                title: '请选择日期',
-                value: cellValue,
+                titleWidget: const Text('请选择日期'),
+                initialValue: cellValue,
                 cellHeight: 80,
                 onConfirm: (value) => customCellSelected.value = value,
                 cellWidget: (context, tdate, selectType) {
                   final today = DateTime.now();
-                  final isToday = tdate.date.millisecondsSinceEpoch ==
-                      DateTime(today.year, today.month, today.day)
-                          .millisecondsSinceEpoch;
+                  final isToday = tdate.date ==
+                      DateTime(today.year, today.month, today.day);
 
                   if (isToday && selectType != DateSelectType.selected) {
                     return _CustomCellContainer(
@@ -336,106 +368,6 @@ Widget _buildStyle(BuildContext context) {
 
                 
 
-不使用Popup
-
-          
-
-
-  
-Widget _buildBlock(BuildContext context) {
-  final selected = ValueNotifier>(
-    [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000],
-  );
-  return Column(
-    crossAxisAlignment: CrossAxisAlignment.start,
-    children: [
-      Row(
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: [
-          TButton(
-            text: '加一个月',
-            theme: TButtonTheme.primary,
-            onTap: () {
-              selected.value = [selected.value[0] + 30 * 24 * 60 * 60 * 1000];
-            },
-          ),
-          const SizedBox(width: 16),
-          TButton(
-            text: '减一个月',
-            theme: TButtonTheme.primary,
-            onTap: () {
-              selected.value = [selected.value[0] - 30 * 24 * 60 * 60 * 1000];
-            },
-          ),
-        ],
-      ),
-      const SizedBox(height: 16),
-      ValueListenableBuilder(
-        valueListenable: selected,
-        builder: (context, value, child) {
-          return TCalendar(
-            title: '请选择日期',
-            value: value,
-            animateTo: true,
-          );
-        },
-      ),
-    ],
-  );
-}
- -
- - - - - -
-Widget _buildBlock(BuildContext context) {
-  final selected = ValueNotifier>(
-    [DateTime.now().millisecondsSinceEpoch + 30 * 24 * 60 * 60 * 1000],
-  );
-  return Column(
-    crossAxisAlignment: CrossAxisAlignment.start,
-    children: [
-      Row(
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: [
-          TButton(
-            text: '加一个月',
-            theme: TButtonTheme.primary,
-            onTap: () {
-              selected.value = [selected.value[0] + 30 * 24 * 60 * 60 * 1000];
-            },
-          ),
-          const SizedBox(width: 16),
-          TButton(
-            text: '减一个月',
-            theme: TButtonTheme.primary,
-            onTap: () {
-              selected.value = [selected.value[0] - 30 * 24 * 60 * 60 * 1000];
-            },
-          ),
-        ],
-      ),
-      const SizedBox(height: 16),
-      ValueListenableBuilder(
-        valueListenable: selected,
-        builder: (context, value, child) {
-          return TCalendar(
-            title: '请选择日期',
-            value: value,
-            animateTo: true,
-          );
-        },
-      ),
-    ],
-  );
-}
- -
- - 农历日历 @@ -443,305 +375,7 @@ Widget _buildBlock(BuildContext context) {
 Widget _buildLunar(BuildContext context) {
-  final dataSource = LunarDataSourceExample();
-  
-  // 当前月份状态
-  final currentMonth = ValueNotifier(
-    DateTime(DateTime.now().year, DateTime.now().month, 1),
-  );
-  
-  // 农历开关状态
-  final showLunarInfo = ValueNotifier(true);
-  
-  // 选中日期
-  final selectedDate = ValueNotifier>([
-    DateTime.now().millisecondsSinceEpoch,
-  ]);
-
-  return Column(
-    crossAxisAlignment: CrossAxisAlignment.start,
-    children: [
-      // 控制栏
-      Container(
-        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
-        decoration: BoxDecoration(
-          color: Colors.grey.shade50,
-          border: Border(
-            bottom: BorderSide(color: Colors.grey.shade200),
-          ),
-        ),
-        child: ValueListenableBuilder(
-          valueListenable: currentMonth,
-          builder: (context, month, child) {
-            // 获取当前月份的农历信息
-            final lunarInfo = dataSource.getLunarInfo(month);
-            final lunarMonth = lunarInfo != null 
-                ? '${lunarInfo.yearText}年 ${lunarInfo.monthText}' 
-                : '';
-
-            return Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                // 农历年月显示
-                if (lunarMonth.isNotEmpty)
-                  Padding(
-                    padding: const EdgeInsets.only(bottom: 8),
-                    child: Text(
-                      lunarMonth,
-                      style: TextStyle(
-                        fontSize: 14,
-                        color: Colors.grey.shade700,
-                        fontWeight: FontWeight.w500,
-                      ),
-                    ),
-                  ),
-                // 按钮行
-                Row(
-                  children: [
-                    // 上一月按钮
-                    TButton(
-                      text: '上一月',
-                      size: TButtonSize.small,
-                      theme: TButtonTheme.primary,
-                      onTap: () {
-                        currentMonth.value = DateTime(
-                          month.year,
-                          month.month - 1,
-                          1,
-                        );
-                        selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                      },
-                    ),
-                    const SizedBox(width: 8),
-                    // 年份选择
-                    Expanded(
-                      child: TButton(
-                        text: '${month.year}年',
-                        size: TButtonSize.small,
-                        theme: TButtonTheme.defaultTheme,
-                        onTap: () async {
-                          final year = await showModalBottomSheet(
-                            context: context,
-                            builder: (context) {
-                              return SizedBox(
-                                height: 300,
-                                child: Column(
-                                  children: [
-                                    const Padding(
-                                      padding: EdgeInsets.all(16),
-                                      child: Text(
-                                        '选择年份',
-                                        style: TextStyle(
-                                          fontSize: 18,
-                                          fontWeight: FontWeight.bold,
-                                        ),
-                                      ),
-                                    ),
-                                    Expanded(
-                                      child: ListView.builder(
-                                        itemCount: 50,
-                                        itemBuilder: (context, index) {
-                                          final year = DateTime.now().year - 10 + index;
-                                          final isSelected = year == month.year;
-                                          return ListTile(
-                                            title: Text(
-                                              '$year年',
-                                              style: TextStyle(
-                                                color: isSelected ? Colors.blue : null,
-                                                fontWeight: isSelected ? FontWeight.bold : null,
-                                              ),
-                                            ),
-                                            onTap: () => Navigator.pop(context, year),
-                                          );
-                                        },
-                                      ),
-                                    ),
-                                  ],
-                                ),
-                              );
-                            },
-                          );
-                          if (year != null) {
-                            currentMonth.value = DateTime(year, month.month, 1);
-                            selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                          }
-                        },
-                      ),
-                    ),
-                    const SizedBox(width: 8),
-                    // 月份选择
-                    Expanded(
-                      child: TButton(
-                        text: '${month.month}月',
-                        size: TButtonSize.small,
-                        theme: TButtonTheme.defaultTheme,
-                        onTap: () async {
-                          final selectedMonth = await showModalBottomSheet(
-                            context: context,
-                            builder: (context) {
-                              return SizedBox(
-                                height: 400,
-                                child: Column(
-                                  children: [
-                                    const Padding(
-                                      padding: EdgeInsets.all(16),
-                                      child: Text(
-                                        '选择月份',
-                                        style: TextStyle(
-                                          fontSize: 18,
-                                          fontWeight: FontWeight.bold,
-                                        ),
-                                      ),
-                                    ),
-                                    Expanded(
-                                      child: GridView.builder(
-                                        padding: const EdgeInsets.all(16),
-                                        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
-                                          crossAxisCount: 3,
-                                          childAspectRatio: 2,
-                                          crossAxisSpacing: 10,
-                                          mainAxisSpacing: 10,
-                                        ),
-                                        itemCount: 12,
-                                        itemBuilder: (context, index) {
-                                          final m = index + 1;
-                                          final isSelected = m == month.month;
-                                          return InkWell(
-                                            onTap: () => Navigator.pop(context, m),
-                                            child: Container(
-                                              alignment: Alignment.center,
-                                              decoration: BoxDecoration(
-                                                color: isSelected ? Colors.blue : Colors.grey.shade200,
-                                                borderRadius: BorderRadius.circular(8),
-                                              ),
-                                              child: Text(
-                                                '$m月',
-                                                style: TextStyle(
-                                                  color: isSelected ? Colors.white : Colors.black,
-                                                  fontWeight: isSelected ? FontWeight.bold : null,
-                                                ),
-                                              ),
-                                            ),
-                                          );
-                                        },
-                                      ),
-                                    ),
-                                  ],
-                                ),
-                              );
-                            },
-                          );
-                          if (selectedMonth != null) {
-                            currentMonth.value = DateTime(month.year, selectedMonth, 1);
-                            selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                          }
-                        },
-                      ),
-                    ),
-                    const SizedBox(width: 8),
-                    // 下一月按钮
-                    TButton(
-                      text: '下一月',
-                      size: TButtonSize.small,
-                      theme: TButtonTheme.primary,
-                      onTap: () {
-                        currentMonth.value = DateTime(
-                          month.year,
-                          month.month + 1,
-                          1,
-                        );
-                        selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                      },
-                    ),
-                    const SizedBox(width: 16),
-                    // 农历开关
-                    ValueListenableBuilder(
-                      valueListenable: showLunarInfo,
-                      builder: (context, show, child) {
-                        return Row(
-                          mainAxisSize: MainAxisSize.min,
-                          children: [
-                            Text(
-                              '农历',
-                              style: TextStyle(fontSize: 12, color: Colors.grey.shade700),
-                            ),
-                            Switch(
-                              value: show,
-                              onChanged: (value) {
-                                showLunarInfo.value = value;
-                              },
-                              materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
-                            ),
-                          ],
-                        );
-                      },
-                    ),
-                  ],
-                ),
-              ],
-            );
-          },
-        ),
-      ),
-      const SizedBox(height: 16),
-      // 日历主体
-      ValueListenableBuilder(
-        valueListenable: showLunarInfo,
-        builder: (context, show, child) {
-          return ValueListenableBuilder(
-            valueListenable: selectedDate,
-            builder: (context, value, child) {
-              return TCalendar(
-                title: '',
-                showLunarInfo: show,
-                dataSource: dataSource,
-                value: value,
-                onChange: (newValue) {
-                  selectedDate.value = newValue;
-                  
-                  // 显示完整农历信息
-                  final date = DateTime.fromMillisecondsSinceEpoch(newValue[0]);
-                  final lunarInfo = dataSource.getLunarInfo(date);
-                  final solarTerm = dataSource.getSolarTerm(date);
-                  final festival = dataSource.getFestival(date, lunarInfo);
-                  final holidayInfo = dataSource.getHolidayInfo(date);
-                  
-                  final buffer = StringBuffer();
-                  buffer.write('阳历:${date.year}年${date.month}月${date.day}日');
-                  
-                  if (lunarInfo != null) {
-                    buffer.write('\n农历:${lunarInfo.monthText}${lunarInfo.dayText}');
-                  }
-                  
-                  if (solarTerm != null && solarTerm.isNotEmpty) {
-                    buffer.write('\n节气:$solarTerm');
-                  }
-                  
-                  if (festival != null && festival.isNotEmpty) {
-                    buffer.write('\n节日:$festival');
-                  }
-                  
-                  if (holidayInfo != null) {
-                    final type = holidayInfo['type'] == 'holiday' ? '假期' : '调休';
-                    buffer.write('\n$type:${holidayInfo['name']}');
-                  }
-                  
-                  ScaffoldMessenger.of(context).clearSnackBars();
-                  ScaffoldMessenger.of(context).showSnackBar(
-                    SnackBar(
-                      content: Text(buffer.toString()),
-                      duration: const Duration(seconds: 3),
-                      behavior: SnackBarBehavior.floating,
-                    ),
-                  );
-                },
-              );
-            },
-          );
-        },
-      ),
-    ],
-  );
+  return const _LunarCalendarDemo();
 }
@@ -752,305 +386,7 @@ Widget _buildLunar(BuildContext context) {
 Widget _buildLunar(BuildContext context) {
-  final dataSource = LunarDataSourceExample();
-  
-  // 当前月份状态
-  final currentMonth = ValueNotifier(
-    DateTime(DateTime.now().year, DateTime.now().month, 1),
-  );
-  
-  // 农历开关状态
-  final showLunarInfo = ValueNotifier(true);
-  
-  // 选中日期
-  final selectedDate = ValueNotifier>([
-    DateTime.now().millisecondsSinceEpoch,
-  ]);
-
-  return Column(
-    crossAxisAlignment: CrossAxisAlignment.start,
-    children: [
-      // 控制栏
-      Container(
-        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
-        decoration: BoxDecoration(
-          color: Colors.grey.shade50,
-          border: Border(
-            bottom: BorderSide(color: Colors.grey.shade200),
-          ),
-        ),
-        child: ValueListenableBuilder(
-          valueListenable: currentMonth,
-          builder: (context, month, child) {
-            // 获取当前月份的农历信息
-            final lunarInfo = dataSource.getLunarInfo(month);
-            final lunarMonth = lunarInfo != null 
-                ? '${lunarInfo.yearText}年 ${lunarInfo.monthText}' 
-                : '';
-
-            return Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                // 农历年月显示
-                if (lunarMonth.isNotEmpty)
-                  Padding(
-                    padding: const EdgeInsets.only(bottom: 8),
-                    child: Text(
-                      lunarMonth,
-                      style: TextStyle(
-                        fontSize: 14,
-                        color: Colors.grey.shade700,
-                        fontWeight: FontWeight.w500,
-                      ),
-                    ),
-                  ),
-                // 按钮行
-                Row(
-                  children: [
-                    // 上一月按钮
-                    TButton(
-                      text: '上一月',
-                      size: TButtonSize.small,
-                      theme: TButtonTheme.primary,
-                      onTap: () {
-                        currentMonth.value = DateTime(
-                          month.year,
-                          month.month - 1,
-                          1,
-                        );
-                        selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                      },
-                    ),
-                    const SizedBox(width: 8),
-                    // 年份选择
-                    Expanded(
-                      child: TButton(
-                        text: '${month.year}年',
-                        size: TButtonSize.small,
-                        theme: TButtonTheme.defaultTheme,
-                        onTap: () async {
-                          final year = await showModalBottomSheet(
-                            context: context,
-                            builder: (context) {
-                              return SizedBox(
-                                height: 300,
-                                child: Column(
-                                  children: [
-                                    const Padding(
-                                      padding: EdgeInsets.all(16),
-                                      child: Text(
-                                        '选择年份',
-                                        style: TextStyle(
-                                          fontSize: 18,
-                                          fontWeight: FontWeight.bold,
-                                        ),
-                                      ),
-                                    ),
-                                    Expanded(
-                                      child: ListView.builder(
-                                        itemCount: 50,
-                                        itemBuilder: (context, index) {
-                                          final year = DateTime.now().year - 10 + index;
-                                          final isSelected = year == month.year;
-                                          return ListTile(
-                                            title: Text(
-                                              '$year年',
-                                              style: TextStyle(
-                                                color: isSelected ? Colors.blue : null,
-                                                fontWeight: isSelected ? FontWeight.bold : null,
-                                              ),
-                                            ),
-                                            onTap: () => Navigator.pop(context, year),
-                                          );
-                                        },
-                                      ),
-                                    ),
-                                  ],
-                                ),
-                              );
-                            },
-                          );
-                          if (year != null) {
-                            currentMonth.value = DateTime(year, month.month, 1);
-                            selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                          }
-                        },
-                      ),
-                    ),
-                    const SizedBox(width: 8),
-                    // 月份选择
-                    Expanded(
-                      child: TButton(
-                        text: '${month.month}月',
-                        size: TButtonSize.small,
-                        theme: TButtonTheme.defaultTheme,
-                        onTap: () async {
-                          final selectedMonth = await showModalBottomSheet(
-                            context: context,
-                            builder: (context) {
-                              return SizedBox(
-                                height: 400,
-                                child: Column(
-                                  children: [
-                                    const Padding(
-                                      padding: EdgeInsets.all(16),
-                                      child: Text(
-                                        '选择月份',
-                                        style: TextStyle(
-                                          fontSize: 18,
-                                          fontWeight: FontWeight.bold,
-                                        ),
-                                      ),
-                                    ),
-                                    Expanded(
-                                      child: GridView.builder(
-                                        padding: const EdgeInsets.all(16),
-                                        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
-                                          crossAxisCount: 3,
-                                          childAspectRatio: 2,
-                                          crossAxisSpacing: 10,
-                                          mainAxisSpacing: 10,
-                                        ),
-                                        itemCount: 12,
-                                        itemBuilder: (context, index) {
-                                          final m = index + 1;
-                                          final isSelected = m == month.month;
-                                          return InkWell(
-                                            onTap: () => Navigator.pop(context, m),
-                                            child: Container(
-                                              alignment: Alignment.center,
-                                              decoration: BoxDecoration(
-                                                color: isSelected ? Colors.blue : Colors.grey.shade200,
-                                                borderRadius: BorderRadius.circular(8),
-                                              ),
-                                              child: Text(
-                                                '$m月',
-                                                style: TextStyle(
-                                                  color: isSelected ? Colors.white : Colors.black,
-                                                  fontWeight: isSelected ? FontWeight.bold : null,
-                                                ),
-                                              ),
-                                            ),
-                                          );
-                                        },
-                                      ),
-                                    ),
-                                  ],
-                                ),
-                              );
-                            },
-                          );
-                          if (selectedMonth != null) {
-                            currentMonth.value = DateTime(month.year, selectedMonth, 1);
-                            selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                          }
-                        },
-                      ),
-                    ),
-                    const SizedBox(width: 8),
-                    // 下一月按钮
-                    TButton(
-                      text: '下一月',
-                      size: TButtonSize.small,
-                      theme: TButtonTheme.primary,
-                      onTap: () {
-                        currentMonth.value = DateTime(
-                          month.year,
-                          month.month + 1,
-                          1,
-                        );
-                        selectedDate.value = [currentMonth.value.millisecondsSinceEpoch];
-                      },
-                    ),
-                    const SizedBox(width: 16),
-                    // 农历开关
-                    ValueListenableBuilder(
-                      valueListenable: showLunarInfo,
-                      builder: (context, show, child) {
-                        return Row(
-                          mainAxisSize: MainAxisSize.min,
-                          children: [
-                            Text(
-                              '农历',
-                              style: TextStyle(fontSize: 12, color: Colors.grey.shade700),
-                            ),
-                            Switch(
-                              value: show,
-                              onChanged: (value) {
-                                showLunarInfo.value = value;
-                              },
-                              materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
-                            ),
-                          ],
-                        );
-                      },
-                    ),
-                  ],
-                ),
-              ],
-            );
-          },
-        ),
-      ),
-      const SizedBox(height: 16),
-      // 日历主体
-      ValueListenableBuilder(
-        valueListenable: showLunarInfo,
-        builder: (context, show, child) {
-          return ValueListenableBuilder(
-            valueListenable: selectedDate,
-            builder: (context, value, child) {
-              return TCalendar(
-                title: '',
-                showLunarInfo: show,
-                dataSource: dataSource,
-                value: value,
-                onChange: (newValue) {
-                  selectedDate.value = newValue;
-                  
-                  // 显示完整农历信息
-                  final date = DateTime.fromMillisecondsSinceEpoch(newValue[0]);
-                  final lunarInfo = dataSource.getLunarInfo(date);
-                  final solarTerm = dataSource.getSolarTerm(date);
-                  final festival = dataSource.getFestival(date, lunarInfo);
-                  final holidayInfo = dataSource.getHolidayInfo(date);
-                  
-                  final buffer = StringBuffer();
-                  buffer.write('阳历:${date.year}年${date.month}月${date.day}日');
-                  
-                  if (lunarInfo != null) {
-                    buffer.write('\n农历:${lunarInfo.monthText}${lunarInfo.dayText}');
-                  }
-                  
-                  if (solarTerm != null && solarTerm.isNotEmpty) {
-                    buffer.write('\n节气:$solarTerm');
-                  }
-                  
-                  if (festival != null && festival.isNotEmpty) {
-                    buffer.write('\n节日:$festival');
-                  }
-                  
-                  if (holidayInfo != null) {
-                    final type = holidayInfo['type'] == 'holiday' ? '假期' : '调休';
-                    buffer.write('\n$type:${holidayInfo['name']}');
-                  }
-                  
-                  ScaffoldMessenger.of(context).clearSnackBars();
-                  ScaffoldMessenger.of(context).showSnackBar(
-                    SnackBar(
-                      content: Text(buffer.toString()),
-                      duration: const Duration(seconds: 3),
-                      behavior: SnackBarBehavior.floating,
-                    ),
-                  );
-                },
-              );
-            },
-          );
-        },
-      ),
-    ],
-  );
+  return const _LunarCalendarDemo();
 }
@@ -1063,43 +399,36 @@ Widget _buildLunar(BuildContext context) { | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| anchorDate | DateTime? | - | 锚点日期 | -| animateTo | bool? | false | 动画滚动到指定位置 | -| bottom | CalendarBottomBuilder? | - | 底部自定义区域构建器,以浮层方式叠加在日历主体之上。 | -| bottomExpanded | ValueListenable? | - | bottom 区域是否展开(响应式)。**仅能在 [TPopupBottomDisplayPanel] 内使用。** | -| cellHeight | double? | 60 | 日期高度 | +| anchorDate | DateTime? | - | 锚点日期,弹出时自动滚动到该日期所在月份。 | +| animateTo | bool | false | 滚动到选中日期/锚点日期所在月份时是否使用动画,默认 false | +| cellHeight | double? | - | 日期单元格高度,默认 60。如需更大行高可传入自定义值(如 80) | | cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | -| dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 | -| dateType | TCalendarDateType | TCalendarDateType.solar | 日历类型:阳历或农历 | -| displayFormat | String? | 'year month' | 年月显示格式,`year`表示年,`month`表示月,如`year month`表示年在前、月在后、中间隔一个空格 | -| firstDayOfWeek | int? | 0 | 第一天从星期几开始,默认 0 = 周日 | -| format | CalendarFormat? | - | 用于格式化日期的函数,可定义日期前后的显示内容和日期样式 | +| dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能。 | +| displayMode | TCalendarDisplayMode | TCalendarDisplayMode.solar | 日历显示模式,控制日期单元格的主/副文本内容: | +| firstDayOfWeek | int | 0 | 第一天从星期几开始,0 = 周日,1 = 周一,…,6 = 周六。默认 0(周日)。 | | height | double? | - | 高度,不传时内嵌模式自动按 5 行日期计算 | +| initialValue | List? | - | 初始选中日期列表,不传则默认今天。 | | key | | - | | -| maxDate | int? | - | 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认 2100-12-31 | -| minDate | int? | - | 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认 1970-01-01 | +| maxDate | DateTime? | - | 最大可选的日期,不传则默认 2100-12-31 | +| minDate | DateTime? | - | 最小可选的日期,不传则默认 1970-01-01 | | monthTitleBuilder | Widget Function(BuildContext context, DateTime monthDate)? | - | 月标题构建器 | -| monthTitleHeight | double? | 22 | 月标题高度 | -| onCellClick | void Function(int value, DateSelectType type, TDate tdate)? | - | 点击日期时触发 | -| onCellLongPress | void Function(int value, DateSelectType type, TDate tdate)? | - | 长按日期时触发 | -| onChange | void Function(List value)? | - | 选中值变化时触发 | -| onHeaderClick | void Function(int index, String week)? | - | 点击周时触发 | +| monthTitleHeight | double | 22 | 每月标题行高度(如 '2025年6月' 所在行),默认 22 | +| onCellClick | void Function(DateTime value, DateSelectType selectType, TDate tdate)? | - | 点击日期时触发 | +| onChange | void Function(List value)? | - | 选中值变化时触发 | | onMonthChange | ValueChanged? | - | 月份变化时触发 | -| showLunarInfo | bool | false | 阳历模式下是否显示农历信息作为副标题 | +| popupBottomBuilder | Widget Function(BuildContext context, List selectedDates)? | - | 弹窗底部自定义区域构建器,以浮层方式叠加在日历主体之上。 | +| popupBottomExpanded | ValueListenable? | - | 弹窗底部区域是否展开(响应式)。**仅在弹窗模式下生效。** | +| safeAreaInset | bool | true | 是否适配底部安全区域(如 iPhone Home Indicator),默认 true | | style | TCalendarStyle? | - | 自定义样式 | -| title | String? | - | 标题 | -| titleWidget | Widget? | - | 标题组件 | -| type | CalendarType? | CalendarType.single | 日历的选择类型,single = 单选;multiple = 多选;range = 区间选择 | -| useSafeArea | bool? | true | 是否使用安全区域(默认 true) | -| value | List? | - | 当前选择的日期(fromMillisecondsSinceEpoch),不传则默认今天,当 type = single 时数组长度为1 | -| width | double? | - | 宽度 | +| titleWidget | Widget? | - | 标题组件,可传入 Text 或自定义 Widget | +| type | CalendarType | CalendarType.single | 日历的选择模式,决定点击日期后的选中行为: | #### 静态方法 | 名称 | 返回类型 | 参数 | 说明 | | --- | --- | --- | --- | -| showPopup | | required BuildContext context, String? title, CalendarType type, List? value, int? minDate, int? maxDate, DateTime? anchorDate, double? fixedHeight, int? firstDayOfWeek, String? displayFormat, double? cellHeight, TCalendarStyle? style, CalendarFormat? format, CalendarBottomBuilder? bottom, ValueListenable? bottomExpanded, Widget? confirmBtn, void Function(List)? onConfirm, VoidCallback? onClose, void Function(int value, DateSelectType type, TDate tdate)? onCellClick, void Function(int value, DateSelectType type, TDate tdate)? onCellLongPress, bool autoClose, bool draggable, Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? cellWidget, TCalendarDateType dateType, TCalendarDataSource? dataSource, bool showLunarInfo, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中日期的毫秒时间戳列表。 ```dart final result = await TCalendar.showPopup( context, title: '请选择日期', type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | +| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget? confirmBtn, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TDate tdate)? onCellClick, bool autoClose, bool draggable, Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? cellWidget, TCalendarDisplayMode displayMode, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | ``` ``` @@ -1117,8 +446,8 @@ Widget _buildLunar(BuildContext context) { | decoration | | - | | | monthTitleStyle | TextStyle? | - | body区域 年月文字样式 | | titleCloseColor | Color? | - | header区域 关闭图标的颜色 | -| titleMaxLine | int? | - | header区域 [TCalendar.title]的行数 | -| titleStyle | TextStyle? | - | header区域 [TCalendar.title]的样式 | +| titleMaxLine | int? | - | header区域 [TCalendar.titleWidget]的行数 | +| titleStyle | TextStyle? | - | header区域 [TCalendar.titleWidget]的样式 | | todayStyle | TextStyle? | - | 当天日期样式 | | weekdayStyle | TextStyle? | - | header区域 周 文字样式 | From b068a4838c67807e4af8ef39c7e1542a15ba6a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Wed, 20 May 2026 10:45:26 +0800 Subject: [PATCH 22/35] =?UTF-8?q?refactor(calendar):=20=E7=B2=BE=E7=AE=80?= =?UTF-8?q?=E5=86=9C=E5=8E=86=E6=97=A5=E5=8E=86=E7=A4=BA=E4=BE=8B=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/lib/lunar_test_main.dart | 27 --- .../lib/page/t_calendar_lunar_demo.dart | 174 ------------------ .../lib/page/t_calendar_lunar_example.dart | 144 --------------- .../lib/page/t_calendar_lunar_test.dart | 161 ---------------- 4 files changed, 506 deletions(-) delete mode 100644 tdesign-component/example/lib/lunar_test_main.dart delete mode 100644 tdesign-component/example/lib/page/t_calendar_lunar_demo.dart delete mode 100644 tdesign-component/example/lib/page/t_calendar_lunar_example.dart delete mode 100644 tdesign-component/example/lib/page/t_calendar_lunar_test.dart diff --git a/tdesign-component/example/lib/lunar_test_main.dart b/tdesign-component/example/lib/lunar_test_main.dart deleted file mode 100644 index 6a0f8276f..000000000 --- a/tdesign-component/example/lib/lunar_test_main.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; -import 'page/t_calendar_lunar_test.dart'; - -/// 农历日历功能快速测试入口 -/// -/// 运行方式: -/// cd /Users/JamesLiauw/Works/WorksMobile/tdesign-flutter/tdesign-component/example -/// ~/flutter/bin/flutter run -d chrome lib/lunar_test_main.dart -void main() { - runApp(const LunarTestApp()); -} - -class LunarTestApp extends StatelessWidget { - const LunarTestApp({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: '农历日历测试', - theme: ThemeData( - primarySwatch: Colors.blue, - useMaterial3: true, - ), - home: const TCalendarLunarTest(), - ); - } -} diff --git a/tdesign-component/example/lib/page/t_calendar_lunar_demo.dart b/tdesign-component/example/lib/page/t_calendar_lunar_demo.dart deleted file mode 100644 index 9e9925d41..000000000 --- a/tdesign-component/example/lib/page/t_calendar_lunar_demo.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:tdesign_flutter/tdesign_flutter.dart'; - -/// 农历日历演示页面 -/// 这是一个独立的演示页面,展示农历功能的基本用法 -class TCalendarLunarDemo extends StatefulWidget { - const TCalendarLunarDemo({Key? key}) : super(key: key); - - @override - State createState() => _TCalendarLunarDemoState(); -} - -class _TCalendarLunarDemoState extends State { - TCalendarDateType _dateType = TCalendarDateType.solar; - bool _showLunarInfo = false; - List _selectedDates = [DateTime.now().millisecondsSinceEpoch]; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('农历日历演示'), - backgroundColor: TTheme.of(context).brandNormalColor, - ), - body: Column( - children: [ - // 控制面板 - Container( - padding: const EdgeInsets.all(16), - color: TTheme.of(context).grayColor1, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '日历模式', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: InkWell( - onTap: () => setState(() => _dateType = TCalendarDateType.solar), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( - color: _dateType == TCalendarDateType.solar ? Colors.blue : Colors.grey[200], - borderRadius: BorderRadius.circular(4), - ), - child: Center(child: Text('阳历模式', style: TextStyle(color: _dateType == TCalendarDateType.solar ? Colors.white : Colors.black))), - ), - ), - ), - Expanded( - child: InkWell( - onTap: () {}, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(4)), - child: Center(child: Text('农历模式', style: TextStyle(color: Colors.grey))), - ), - ), - ), - ], - ), - const SizedBox(height: 16), - TSwitch( - enable: _dateType == TCalendarDateType.solar, - isOn: _showLunarInfo, - size: TSwitchSize.large, - onChanged: (value) { - if (value != null && value != _showLunarInfo) { - setState(() { - _showLunarInfo = value; - }); - } - return true; - }, - ), - const SizedBox(height: 4), - Text( - '阳历模式下显示农历副标题', - style: TextStyle( - fontSize: 14, - color: TTheme.of(context).fontGyColor3, - ), - ), - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: TTheme.of(context).brandColor1, - borderRadius: BorderRadius.circular(6), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '💡 提示', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 4), - Text( - _dateType == TCalendarDateType.lunar - ? '农历模式需要实现 TCalendarDataSource 接口\n请参考 lunar_data_source_example.dart' - : _showLunarInfo - ? '当前显示阳历日期,下方显示农历信息\n需要实现 TCalendarDataSource 接口' - : '当前仅显示阳历日期(默认模式)', - style: TextStyle( - fontSize: 12, - color: TTheme.of(context).fontGyColor3, - ), - ), - ], - ), - ), - ], - ), - ), - // 日历组件 - Expanded( - child: TCalendar( - type: CalendarType.single, - value: _selectedDates, - dateType: _dateType, - // dataSource: null, // 实际使用时需要提供数据源 - showLunarInfo: _showLunarInfo, - onChange: (dates) { - setState(() { - _selectedDates = dates; - }); - }, - ), - ), - // 选中日期显示 - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 8, - offset: const Offset(0, -2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '已选日期', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text( - _selectedDates.isEmpty - ? '未选择' - : _formatDate( - DateTime.fromMillisecondsSinceEpoch(_selectedDates[0])), - style: const TextStyle(fontSize: 18), - ), - ], - ), - ), - ], - ), - ); - } - - String _formatDate(DateTime date) { - return '${date.year}年${date.month}月${date.day}日'; - } -} diff --git a/tdesign-component/example/lib/page/t_calendar_lunar_example.dart b/tdesign-component/example/lib/page/t_calendar_lunar_example.dart deleted file mode 100644 index a0d1246e7..000000000 --- a/tdesign-component/example/lib/page/t_calendar_lunar_example.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:tdesign_flutter/tdesign_flutter.dart'; -import '../lunar_data_source_example.dart'; - -/// 农历日历示例页面 -/// -/// 展示如何使用 TCalendar 的农历功能 -class TCalendarLunarExample extends StatefulWidget { - const TCalendarLunarExample({Key? key}) : super(key: key); - - @override - State createState() => _TCalendarLunarExampleState(); -} - -class _TCalendarLunarExampleState extends State { - TCalendarDateType _dateType = TCalendarDateType.solar; - bool _showLunarInfo = true; // 默认显示农历信息 - List _selectedDates = []; - final _dataSource = LunarDataSourceExample(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('农历日历示例'), - ), - body: Column( - children: [ - // 控制面板 - _buildControlPanel(), - - // 日历组件 - Expanded( - child: TCalendar( - dateType: _dateType, - dataSource: _dataSource, - showLunarInfo: _showLunarInfo, - value: _selectedDates, - onChange: (dates) { - setState(() { - _selectedDates = dates; - }); - }, - ), - ), - - // 选中日期显示 - _buildSelectedInfo(), - ], - ), - ); - } - - Widget _buildControlPanel() { - return Container( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '日历类型:', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: RadioListTile( - title: const Text('阳历'), - value: TCalendarDateType.solar, - groupValue: _dateType, - onChanged: (value) { - setState(() { - _dateType = value!; - }); - }, - ), - ), - Expanded( - child: RadioListTile( - title: const Text('农历'), - value: TCalendarDateType.lunar, - groupValue: _dateType, - onChanged: (value) { - setState(() { - _dateType = value!; - }); - }, - ), - ), - ], - ), - const SizedBox(height: 16), - SwitchListTile( - title: const Text('阳历模式下显示农历信息'), - value: _showLunarInfo, - onChanged: _dateType == TCalendarDateType.solar - ? (value) { - setState(() { - _showLunarInfo = value; - }); - } - : null, - ), - const Divider(), - ], - ), - ); - } - - Widget _buildSelectedInfo() { - if (_selectedDates.isEmpty) { - return Container( - padding: const EdgeInsets.all(16), - child: const Text('请选择日期'), - ); - } - - final date = DateTime.fromMillisecondsSinceEpoch(_selectedDates.first); - final dataSource = LunarDataSourceExample(); - final lunarInfo = dataSource.getLunarInfo(date); - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey[100], - border: Border(top: BorderSide(color: Colors.grey[300]!)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '已选择日期:', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: 8), - Text('阳历:${date.year}年${date.month}月${date.day}日'), - if (lunarInfo != null) - Text('农历:${lunarInfo.yearText}年 ${lunarInfo.monthText}${lunarInfo.dayText}'), - ], - ), - ); - } -} diff --git a/tdesign-component/example/lib/page/t_calendar_lunar_test.dart b/tdesign-component/example/lib/page/t_calendar_lunar_test.dart deleted file mode 100644 index 42dc59876..000000000 --- a/tdesign-component/example/lib/page/t_calendar_lunar_test.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:tdesign_flutter/tdesign_flutter.dart'; -import '../lunar_data_source_example.dart'; - -/// 农历日历功能快速测试页面 -class TCalendarLunarTest extends StatefulWidget { - const TCalendarLunarTest({Key? key}) : super(key: key); - - @override - State createState() => _TCalendarLunarTestState(); -} - -class _TCalendarLunarTestState extends State { - bool _showLunarInfo = true; - final _dataSource = LunarDataSourceExample(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text( - '农历日历测试', - style: TextStyle(color: Colors.white), - ), - backgroundColor: const Color(0xFF0052D9), - ), - body: Column( - children: [ - // 快速切换 - Container( - padding: const EdgeInsets.all(16), - color: Colors.grey[100], - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - '显示农历信息', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - Switch( - value: _showLunarInfo, - onChanged: (value) { - setState(() { - _showLunarInfo = value; - }); - }, - activeColor: const Color(0xFF0052D9), - ), - ], - ), - ), - - // 提示信息 - Container( - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: const Color(0xFFE8F3FF), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: const Color(0xFF0052D9).withOpacity(0.2)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Row( - children: [ - Icon(Icons.info_outline, color: Color(0xFF0052D9), size: 20), - SizedBox(width: 8), - Text( - '功能说明', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Color(0xFF0052D9), - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - _showLunarInfo - ? '✅ 农历信息已启用\n每个日期下方会显示对应的农历日期' - : '❌ 农历信息已关闭\n仅显示阳历日期(默认模式)', - style: const TextStyle(fontSize: 13, height: 1.5), - ), - ], - ), - ), - - // 日历组件 - Expanded( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: TCalendar( - type: CalendarType.single, - dateType: TCalendarDateType.solar, - dataSource: _dataSource, - showLunarInfo: _showLunarInfo, - cellHeight: _showLunarInfo ? 80 : 60, // 增加到 80 以完全避免溢出 - value: [DateTime.now().millisecondsSinceEpoch], - onChange: (dates) { - if (dates.isNotEmpty) { - final date = DateTime.fromMillisecondsSinceEpoch(dates[0]); - final lunarInfo = _dataSource.getLunarInfo(date); - final solarTerm = _dataSource.getSolarTerm(date); - final festival = _dataSource.getFestival(date); - final holidayInfo = _dataSource.getHolidayInfo(date); - - // 构建消息 - final buffer = StringBuffer(); - buffer.write('选中日期:\n'); - buffer.write('阳历:${date.year}年${date.month}月${date.day}日\n'); - - if (lunarInfo != null) { - buffer.write('农历:${lunarInfo.yearText}年${lunarInfo.monthText}${lunarInfo.dayText}'); - } - - // 显示节气 - if (solarTerm != null && solarTerm.isNotEmpty) { - buffer.write('\n节气:$solarTerm'); - } - - // 显示节日 - if (festival != null && festival.isNotEmpty) { - buffer.write('\n节日:$festival'); - } - - // 显示假期信息 - if (holidayInfo != null && holidayInfo.isNotEmpty) { - buffer.write('\n假期:$holidayInfo'); - } - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(buffer.toString()), - duration: const Duration(seconds: 3), - ), - ); - } - }, - ), - ), - ), - - const SizedBox(height: 16), - ], - ), - ); - } -} From 81d66b4a77c4477c1ea757653d7ad35c9d54719c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Wed, 20 May 2026 11:20:43 +0800 Subject: [PATCH 23/35] =?UTF-8?q?feat(calendar):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E7=A1=AE=E8=AE=A4=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=B9=B6=E6=9B=B4=E6=96=B0=E6=97=A5=E5=8E=86?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tdesign-component/demo_tool/all_build.sh | 2 +- .../assets/code/calendar._buildStyle.txt | 43 +++-- .../example/lib/page/t_calendar_page.dart | 43 +++-- .../src/components/calendar/t_calendar.dart | 95 ++++++++--- tdesign-component/test/t_calendar_test.dart | 155 ++++++++++++++++++ 5 files changed, 292 insertions(+), 46 deletions(-) diff --git a/tdesign-component/demo_tool/all_build.sh b/tdesign-component/demo_tool/all_build.sh index 0a881ce83..4940b4d08 100644 --- a/tdesign-component/demo_tool/all_build.sh +++ b/tdesign-component/demo_tool/all_build.sh @@ -40,7 +40,7 @@ dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/compo # 输入 # calendar -dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/calendar" --name TCalendar,TCalendarPopup,TCalendarStyle,TCalendarDataSource,TLunarInfo,TCalendarDateType --folder-name calendar --output "$PARENT_DIR/example/assets/api/" --only-api +dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/calendar" --name TCalendar,TCalendarStyle,TCalendarDataSource,TLunarInfo,TCalendarDateType --folder-name calendar --output "$PARENT_DIR/example/assets/api/" --only-api # cascader dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/cascader" --name TMultiCascader --folder-name cascader --output "$PARENT_DIR/example/assets/api/" --only-api diff --git a/tdesign-component/example/assets/code/calendar._buildStyle.txt b/tdesign-component/example/assets/code/calendar._buildStyle.txt index f5ef2f4f1..31cd453d7 100644 --- a/tdesign-component/example/assets/code/calendar._buildStyle.txt +++ b/tdesign-component/example/assets/code/calendar._buildStyle.txt @@ -8,25 +8,38 @@ Widget _buildStyle(BuildContext context) { 15: '元宵节', }; + final customTextSelected = + ValueNotifier>([DateTime(2022, 1, 15)]); + final customBtnSelected = + ValueNotifier>([DateTime.now()]); final customCellSelected = ValueNotifier>( [DateTime.now().add(const Duration(days: 30))]); return ValueListenableBuilder( - valueListenable: customCellSelected, - builder: (context, cellValue, _) { - final cellDate = cellValue[0]; - return TCellGroup( - cells: [ + valueListenable: customTextSelected, + builder: (context, textSelected, _) { + return ValueListenableBuilder( + valueListenable: customBtnSelected, + builder: (context, btnSelected, _) { + return ValueListenableBuilder( + valueListenable: customCellSelected, + builder: (context, cellValue, _) { + final cellDate = cellValue[0]; + return TCellGroup( + cells: [ // 1. 自定义文案(cellWidget 回调自定义 cell 渲染) TCell( title: '自定义文案', arrow: true, - onClick: (cell) { + note: _formatYmd(textSelected), + onClick: (_) { TCalendar.showPopup( context, titleWidget: const Text('请选择日期'), + initialValue: textSelected, minDate: DateTime(2022, 1, 1), maxDate: DateTime(2022, 2, 15), + onConfirm: (value) => customTextSelected.value = value, cellWidget: (context, tdate, selectType) { final isSpecial = tdate.date.month == 2 && map.keys.contains(tdate.date.day); @@ -73,23 +86,25 @@ Widget _buildStyle(BuildContext context) { TCell( title: '自定义按钮', arrow: true, - onClick: (cell) { + note: _formatYmd(btnSelected), + onClick: (_) { TCalendar.showPopup( context, titleWidget: const Text('请选择日期'), - initialValue: [DateTime.now()], - confirmBtn: Padding( + initialValue: btnSelected, + confirmBtnBuilder: (onConfirm) => Padding( padding: EdgeInsets.symmetric( vertical: TTheme.of(context).spacer16), - child: const TButton( + child: TButton( theme: TButtonTheme.danger, shape: TButtonShape.round, text: 'ok', isBlock: true, size: TButtonSize.large, + onTap: onConfirm, ), ), - onConfirm: (value) => print('confirmed: $value'), + onConfirm: (value) => customBtnSelected.value = value, ); }, ), @@ -152,7 +167,11 @@ Widget _buildStyle(BuildContext context) { ); }, ), - ], + ], + ); + }, + ); + }, ); }, ); diff --git a/tdesign-component/example/lib/page/t_calendar_page.dart b/tdesign-component/example/lib/page/t_calendar_page.dart index 922e6f46f..4ad5a0560 100644 --- a/tdesign-component/example/lib/page/t_calendar_page.dart +++ b/tdesign-component/example/lib/page/t_calendar_page.dart @@ -878,25 +878,38 @@ Widget _buildStyle(BuildContext context) { 15: '元宵节', }; + final customTextSelected = + ValueNotifier>([DateTime(2022, 1, 15)]); + final customBtnSelected = + ValueNotifier>([DateTime.now()]); final customCellSelected = ValueNotifier>( [DateTime.now().add(const Duration(days: 30))]); return ValueListenableBuilder( - valueListenable: customCellSelected, - builder: (context, cellValue, _) { - final cellDate = cellValue[0]; - return TCellGroup( - cells: [ + valueListenable: customTextSelected, + builder: (context, textSelected, _) { + return ValueListenableBuilder( + valueListenable: customBtnSelected, + builder: (context, btnSelected, _) { + return ValueListenableBuilder( + valueListenable: customCellSelected, + builder: (context, cellValue, _) { + final cellDate = cellValue[0]; + return TCellGroup( + cells: [ // 1. 自定义文案(cellWidget 回调自定义 cell 渲染) TCell( title: '自定义文案', arrow: true, - onClick: (cell) { + note: _formatYmd(textSelected), + onClick: (_) { TCalendar.showPopup( context, titleWidget: const Text('请选择日期'), + initialValue: textSelected, minDate: DateTime(2022, 1, 1), maxDate: DateTime(2022, 2, 15), + onConfirm: (value) => customTextSelected.value = value, cellWidget: (context, tdate, selectType) { final isSpecial = tdate.date.month == 2 && map.keys.contains(tdate.date.day); @@ -943,23 +956,25 @@ Widget _buildStyle(BuildContext context) { TCell( title: '自定义按钮', arrow: true, - onClick: (cell) { + note: _formatYmd(btnSelected), + onClick: (_) { TCalendar.showPopup( context, titleWidget: const Text('请选择日期'), - initialValue: [DateTime.now()], - confirmBtn: Padding( + initialValue: btnSelected, + confirmBtnBuilder: (onConfirm) => Padding( padding: EdgeInsets.symmetric( vertical: TTheme.of(context).spacer16), - child: const TButton( + child: TButton( theme: TButtonTheme.danger, shape: TButtonShape.round, text: 'ok', isBlock: true, size: TButtonSize.large, + onTap: onConfirm, ), ), - onConfirm: (value) => print('confirmed: $value'), + onConfirm: (value) => customBtnSelected.value = value, ); }, ), @@ -1022,7 +1037,11 @@ Widget _buildStyle(BuildContext context) { ); }, ), - ], + ], + ); + }, + ); + }, ); }, ); diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart index d0155c57c..6332e36ab 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart @@ -30,6 +30,7 @@ class TCalendarInherited extends InheritedWidget { this.popupConfirmBtn, this.onConfirm, this.confirmBtn, + this.confirmBtnBuilder, Key? key, }) : super(child: child, key: key); @@ -60,8 +61,13 @@ class TCalendarInherited extends InheritedWidget { bool get effectivePopupConfirmBtn => popupConfirmBtn ?? popupControls; final VoidCallback? onConfirm; + + /// 自定义确认按钮(静态 Widget,需自行处理点击;推荐 [confirmBtnBuilder])。 final Widget? confirmBtn; + /// 自定义确认按钮构建器,[onConfirm] 与默认确认按钮行为一致(回传选中值并关闭弹窗)。 + final Widget Function(VoidCallback onConfirm)? confirmBtnBuilder; + @override bool updateShouldNotify(covariant TCalendarInherited oldWidget) => false; @@ -337,9 +343,15 @@ class TCalendar extends StatefulWidget { /// 弹窗底部区域是否展开(响应式) ValueListenable? popupBottomExpanded, - /// 自定义确认按钮 + /// 自定义确认按钮(静态 Widget)。 + /// + /// 若未设置 [onTap],[showPopup] 会自动为其叠加点击层以触发确认; + /// 需要按钮按压态时请改用 [confirmBtnBuilder]。 Widget? confirmBtn, + /// 自定义确认按钮构建器,[onConfirm] 回调与默认确认按钮一致。 + Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, + /// 点击确认按钮时触发 void Function(List)? onConfirm, @@ -404,12 +416,20 @@ class TCalendar extends StatefulWidget { panelTitle = _extractTextFromWidget(titleWidget); } + final effectiveConfirmBtnBuilder = confirmBtnBuilder ?? + (confirmBtn != null + ? (onConfirm) => _CalendarConfirmTapProxy( + onConfirm: onConfirm, + child: confirmBtn, + ) + : null); + return TCalendarInherited( selected: selected, usePopup: true, popupControls: false, popupConfirmBtn: true, - confirmBtn: confirmBtn, + confirmBtnBuilder: effectiveConfirmBtnBuilder, onClose: () { if (autoClose) { doClose(nav); @@ -671,27 +691,35 @@ class _TCalendarState extends State { Expanded( child: _buildBodyArea(verticalGap, hasBottom, bottomExpanded), ), - if (_showPopupConfirmBtn) - inherited?.confirmBtn ?? - Padding( - padding: widget.safeAreaInset - ? EdgeInsets.only(top: TTheme.of(context).spacer16) - : EdgeInsets.symmetric( - vertical: TTheme.of(context).spacer16), - child: TButton( - theme: TButtonTheme.primary, - text: context.resource.confirm, - isBlock: true, - size: TButtonSize.large, - onTap: inherited?.onConfirm, - ), - ), + if (_showPopupConfirmBtn) _buildConfirmBtnArea(context), if (widget.safeAreaInset) SizedBox(height: MediaQuery.of(context).padding.bottom) ], ); } + Widget _buildConfirmBtnArea(BuildContext context) { + final onConfirm = inherited?.onConfirm; + if (inherited?.confirmBtnBuilder != null) { + return inherited!.confirmBtnBuilder!(onConfirm ?? () {}); + } + if (inherited?.confirmBtn != null) { + return inherited!.confirmBtn!; + } + return Padding( + padding: widget.safeAreaInset + ? EdgeInsets.only(top: TTheme.of(context).spacer16) + : EdgeInsets.symmetric(vertical: TTheme.of(context).spacer16), + child: TButton( + theme: TButtonTheme.primary, + text: context.resource.confirm, + isBlock: true, + size: TButtonSize.large, + onTap: onConfirm, + ), + ); + } + Widget _buildBodyArea( double verticalGap, bool hasBottom, @@ -929,10 +957,9 @@ class _TCalendarState extends State { final btnPadding = widget.safeAreaInset ? TTheme.of(context).spacer16 : TTheme.of(context).spacer16 * 2; - // 仅默认确认按钮使用固定高度;自定义 confirmBtn 由调用方保证 bottom 不重叠。 - final btnHeight = - inherited?.confirmBtn == null ? _confirmBtnHeight : 0.0; - return safeBottom + btnPadding + btnHeight; + // 默认与自定义 confirmBtn 均预留固定高度,避免 popupBottomBuilder 浮层重叠。 + // 若自定义按钮更高,请在 popupHeight 中额外预留空间。 + return safeBottom + btnPadding + _confirmBtnHeight; } return safeBottom; @@ -964,3 +991,29 @@ class _TCalendarState extends State { bodyPadding * 2; } } + +/// 为未绑定 [onTap] 的静态 [confirmBtn] 叠加透明点击层,触发确认并关闭弹窗。 +class _CalendarConfirmTapProxy extends StatelessWidget { + const _CalendarConfirmTapProxy({ + required this.onConfirm, + required this.child, + }); + + final VoidCallback onConfirm; + final Widget child; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + child, + Positioned.fill( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onConfirm, + ), + ), + ], + ); + } +} diff --git a/tdesign-component/test/t_calendar_test.dart b/tdesign-component/test/t_calendar_test.dart index 966a0bc44..731206f1d 100644 --- a/tdesign-component/test/t_calendar_test.dart +++ b/tdesign-component/test/t_calendar_test.dart @@ -439,4 +439,159 @@ void main() { expect(find.text('15'), findsOneWidget); }); }); + + // ----------------------------------------------------------------------- + // showPopup 集成 + // ----------------------------------------------------------------------- + group('TCalendar.showPopup', () { + testWidgets('选中新日期并确认后返回选中列表', (tester) async { + final day15 = _day(2024, 6, 15); + final day20 = _day(2024, 6, 20); + List? popupResult; + + await tester.pumpWidget( + _buildTestApp( + Builder( + builder: (context) => ElevatedButton( + onPressed: () async { + popupResult = await TCalendar.showPopup( + context, + titleWidget: const Text('选择日期'), + type: CalendarType.single, + initialValue: [day15], + minDate: _day(2024, 6, 1), + maxDate: _day(2024, 6, 30), + ); + }, + child: const Text('open'), + ), + ), + ), + ); + + await tester.tap(find.text('open')); + await tester.pumpAndSettle(); + + expect(find.text('选择日期'), findsOneWidget); + + await tester.tap(find.text('20')); + await tester.pump(); + + await tester.tap(find.text('确定')); + await tester.pumpAndSettle(); + + expect(popupResult, isNotNull); + expect(popupResult!.length, 1); + expect(popupResult!.first, day20); + }); + + testWidgets('点击关闭按钮未确认时返回 null', (tester) async { + List? popupResult; + + await tester.pumpWidget( + _buildTestApp( + Builder( + builder: (context) => ElevatedButton( + onPressed: () async { + popupResult = await TCalendar.showPopup( + context, + titleWidget: const Text('选择日期'), + type: CalendarType.single, + minDate: _day(2024, 6, 1), + maxDate: _day(2024, 6, 30), + ); + }, + child: const Text('open'), + ), + ), + ), + ); + + await tester.tap(find.text('open')); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(IconButton)); + await tester.pumpAndSettle(); + + expect(popupResult, isNull); + }); + + testWidgets('自定义 confirmBtn 点击后返回选中值并关闭弹窗', (tester) async { + final day15 = _day(2024, 6, 15); + final day20 = _day(2024, 6, 20); + List? popupResult; + + await tester.pumpWidget( + _buildTestApp( + Builder( + builder: (context) => ElevatedButton( + onPressed: () async { + popupResult = await TCalendar.showPopup( + context, + titleWidget: const Text('选择日期'), + type: CalendarType.single, + initialValue: [day15], + minDate: _day(2024, 6, 1), + maxDate: _day(2024, 6, 30), + confirmBtn: const Padding( + padding: EdgeInsets.all(16), + child: Text('ok'), + ), + ); + }, + child: const Text('open'), + ), + ), + ), + ); + + await tester.tap(find.text('open')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('20')); + await tester.pump(); + + await tester.tap(find.text('ok')); + await tester.pumpAndSettle(); + + expect(popupResult, isNotNull); + expect(popupResult!.single, day20); + }); + + testWidgets('popupBottomBuilder 与确认按钮区域不重叠', (tester) async { + await tester.pumpWidget( + _buildTestApp( + Builder( + builder: (context) => ElevatedButton( + onPressed: () async { + await TCalendar.showPopup( + context, + titleWidget: const Text('选择日期'), + type: CalendarType.single, + minDate: _day(2024, 6, 1), + maxDate: _day(2024, 6, 30), + popupHeight: 640, + popupBottomBuilder: (_, __) => const Text('底部区域'), + ); + }, + child: const Text('open'), + ), + ), + ), + ); + + await tester.tap(find.text('open')); + await tester.pumpAndSettle(); + + final positioned = tester + .widgetList( + find.ancestor( + of: find.text('底部区域'), + matching: find.byType(Positioned), + ), + ) + .firstWhere((p) => (p.bottom ?? 0) > 0); + expect(positioned.bottom, greaterThan(0)); + }); + }); } From eccc786437e331cc9196c69b0ab6b1376e0deb2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Wed, 20 May 2026 11:25:21 +0800 Subject: [PATCH 24/35] =?UTF-8?q?feat(calendar):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E5=BA=95=E9=83=A8=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E5=8C=BA=E5=9F=9F=E6=94=AF=E6=8C=81=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E9=80=BB=E8=BE=91=E5=92=8C=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/calendar/t_calendar.dart | 105 ++++++------------ tdesign-component/test/t_calendar_test.dart | 64 +++++++---- 2 files changed, 71 insertions(+), 98 deletions(-) diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart index 6332e36ab..d84518565 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart @@ -31,8 +31,14 @@ class TCalendarInherited extends InheritedWidget { this.onConfirm, this.confirmBtn, this.confirmBtnBuilder, + this.popupBottomBuilder, + this.popupBottomExpanded, Key? key, - }) : super(child: child, key: key); + }) : assert( + popupBottomExpanded == null || popupBottomBuilder != null, + 'popupBottomExpanded 需配合 popupBottomBuilder 使用', + ), + super(child: child, key: key); final VoidCallback? onClose; @@ -68,6 +74,14 @@ class TCalendarInherited extends InheritedWidget { /// 自定义确认按钮构建器,[onConfirm] 与默认确认按钮行为一致(回传选中值并关闭弹窗)。 final Widget Function(VoidCallback onConfirm)? confirmBtnBuilder; + /// 弹窗底部自定义区域构建器(仅弹窗模式,由 [TCalendar.showPopup] 或手动 + /// [TCalendarInherited] 注入)。 + final Widget Function(BuildContext context, List selectedDates)? + popupBottomBuilder; + + /// 弹窗底部区域是否展开(响应式),需配合 [popupBottomBuilder]。 + final ValueListenable? popupBottomExpanded; + @override bool updateShouldNotify(covariant TCalendarInherited oldWidget) => false; @@ -119,8 +133,6 @@ class TCalendar extends StatefulWidget { this.onChange, this.onCellClick, this.safeAreaInset = true, - this.popupBottomBuilder, - this.popupBottomExpanded, this.monthTitleHeight = 22, this.monthTitleBuilder, this.animateTo = false, @@ -129,11 +141,7 @@ class TCalendar extends StatefulWidget { this.anchorDate, this.displayMode = TCalendarDisplayMode.solar, this.dataSource, - }) : assert( - popupBottomExpanded == null || popupBottomBuilder != null, - 'popupBottomExpanded 需配合 popupBottomBuilder 使用', - ), - super(key: key); + }) : super(key: key); /// 第一天从星期几开始,0 = 周日,1 = 周一,…,6 = 周六。默认 0(周日)。 final int firstDayOfWeek; @@ -186,44 +194,6 @@ class TCalendar extends StatefulWidget { /// 是否适配底部安全区域(如 iPhone Home Indicator),默认 true final bool safeAreaInset; - /// 弹窗底部自定义区域构建器,以浮层方式叠加在日历主体之上。 - /// - /// **仅在弹窗模式下生效**(即 [TCalendar] 作为 [TPopupBottomDisplayPanel] 的子树时)。 - /// 非弹窗模式下传入此参数将被忽略。 - /// - /// - **不会撑高 [TCalendar]**,请在 `popupHeight` 中预留 bottom 自身的占用高度; - /// - `selectedDates` 随弹窗内日期点击实时更新; - /// - 传入的 `selectedDates` 是只读视图([List.unmodifiable]),请勿原地修改。 - /// - /// ```dart - /// TPopupBottomDisplayPanel( - /// fixedHeight: 600, - /// child: TCalendar( - /// popupBottomBuilder: (ctx, dates) => MyFooter(selectedDates: dates), - /// ), - /// ); - /// ``` - final Widget Function(BuildContext context, List selectedDates)? - popupBottomBuilder; - - /// 弹窗底部区域是否展开(响应式)。**仅在弹窗模式下生效。** - /// - /// 为 `null`(默认)时 bottom 始终展开;传入 [ValueListenable] 时, - /// bottom 展开/收起将跟随其值变化播放滑动动画,常配合 [ValueNotifier] 使用。 - /// - /// ```dart - /// final expanded = ValueNotifier(false); - /// TPopupBottomDisplayPanel( - /// fixedHeight: 600, - /// child: TCalendar( - /// popupBottomExpanded: expanded, - /// onCellClick: (v, t, d) => expanded.value = true, - /// popupBottomBuilder: (ctx, dates) => MyFooter(), - /// ), - /// ); - /// ``` - final ValueListenable? popupBottomExpanded; - /// 每月标题行高度(如 '2025年6月' 所在行),默认 22 final double monthTitleHeight; @@ -336,11 +306,11 @@ class TCalendar extends StatefulWidget { /// 自定义样式 TCalendarStyle? style, - /// 弹窗底部自定义区域构建器 + /// 弹窗底部自定义区域构建器(经 [TCalendarInherited] 注入,仅弹窗内生效)。 Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, - /// 弹窗底部区域是否展开(响应式) + /// 弹窗底部区域是否展开(响应式),需配合 [popupBottomBuilder]。 ValueListenable? popupBottomExpanded, /// 自定义确认按钮(静态 Widget)。 @@ -430,6 +400,8 @@ class TCalendar extends StatefulWidget { popupControls: false, popupConfirmBtn: true, confirmBtnBuilder: effectiveConfirmBtnBuilder, + popupBottomBuilder: popupBottomBuilder, + popupBottomExpanded: popupBottomExpanded, onClose: () { if (autoClose) { doClose(nav); @@ -461,8 +433,6 @@ class TCalendar extends StatefulWidget { firstDayOfWeek: firstDayOfWeek, cellHeight: cellHeight, style: style, - popupBottomBuilder: popupBottomBuilder, - popupBottomExpanded: popupBottomExpanded, onCellClick: onCellClick, cellWidget: cellWidget, displayMode: displayMode, @@ -528,8 +498,6 @@ class _TCalendarState extends State { bool _initializedSelected = false; - bool get _usePopupBottom => inherited?.usePopup == true; - /// 解析 displayMode 为旧的 dateType 和 showLunarInfo TCalendarDateType get _dateType { switch (widget.displayMode) { @@ -593,17 +561,6 @@ class _TCalendarState extends State { _cachedValueDates = widget._value; } - void _assertPopupOnlyBottom() { - assert( - widget.popupBottomBuilder == null || _usePopupBottom, - '[TCalendar] popupBottomBuilder 仅能在弹窗模式下使用', - ); - assert( - widget.popupBottomExpanded == null || _usePopupBottom, - '[TCalendar] popupBottomExpanded 仅能在弹窗模式下使用', - ); - } - // 仅在非 build phase 调用。 void _syncSelectedToInheritedSync() { if (inherited == null) { @@ -627,9 +584,11 @@ class _TCalendarState extends State { @override Widget build(BuildContext context) { - _assertPopupOnlyBottom(); final verticalGap = _style.verticalGap ?? TTheme.of(context).spacer8; - final hasBottom = widget.popupBottomBuilder != null && _usePopupBottom; + final popupBottomBuilder = inherited?.popupBottomBuilder; + final popupBottomExpanded = inherited?.popupBottomExpanded; + final hasBottom = + inherited?.usePopup == true && popupBottomBuilder != null; Widget stackContent(bool bottomExpanded) { return Stack( @@ -641,9 +600,9 @@ class _TCalendarState extends State { ); } - final child = hasBottom && widget.popupBottomExpanded != null + final child = hasBottom && popupBottomExpanded != null ? ValueListenableBuilder( - valueListenable: widget.popupBottomExpanded!, + valueListenable: popupBottomExpanded, builder: (context, expanded, _) => stackContent(expanded), ) : stackContent(hasBottom); @@ -729,7 +688,7 @@ class _TCalendarState extends State { if (!hasBottom) { return body; } - if (widget.popupBottomExpanded == null) { + if (inherited?.popupBottomExpanded == null) { return Padding( padding: const EdgeInsets.only(bottom: _bottomPeekHeight), child: body, @@ -905,24 +864,22 @@ class _TCalendarState extends State { return [tapped]; } - // 行为约定详见 [TCalendar.popupBottomBuilder]。 + // 行为约定详见 [TCalendarInherited.popupBottomBuilder]。 Widget _buildBottom(bool bottomExpanded) { - assert(widget.popupBottomBuilder != null); - assert(inherited != null); - + final popupBottomBuilder = inherited!.popupBottomBuilder!; final bottomOffset = _calcBottomOffset(); final content = ValueListenableBuilder>( valueListenable: inherited!.selected, builder: (context, selectedDates, _) { - return widget.popupBottomBuilder!( + return popupBottomBuilder( context, List.unmodifiable(selectedDates), ); }, ); - if (widget.popupBottomExpanded != null) { + if (inherited!.popupBottomExpanded != null) { return Positioned( left: 0, right: 0, diff --git a/tdesign-component/test/t_calendar_test.dart b/tdesign-component/test/t_calendar_test.dart index 731206f1d..5de19b246 100644 --- a/tdesign-component/test/t_calendar_test.dart +++ b/tdesign-component/test/t_calendar_test.dart @@ -16,28 +16,48 @@ DateTime _day(int y, int m, int d) => DateTime(y, m, d); void main() { // ----------------------------------------------------------------------- - // popupBottomBuilder / popupBottomExpanded + // popupBottomBuilder / popupBottomExpanded(仅 TCalendarInherited / showPopup) // ----------------------------------------------------------------------- group('TCalendar — popupBottom / popupBottomExpanded', () { test('popupBottomExpanded 未配合 popupBottomBuilder 时触发 assert', () { expect( - () => TCalendar(popupBottomExpanded: ValueNotifier(false)), + () => TCalendarInherited( + selected: ValueNotifier>([]), + popupBottomExpanded: ValueNotifier(false), + child: const SizedBox(), + ), throwsAssertionError, ); }); - testWidgets('非弹窗模式使用 popupBottomBuilder 触发 assert', (tester) async { + testWidgets('内嵌模式无法通过 TCalendar 传入 popupBottomBuilder', (tester) async { await tester.pumpWidget( _buildTestApp( TCalendar( titleWidget: const Text('测试'), height: 640, + ), + ), + ); + + expect(find.text('底部'), findsNothing); + }); + + testWidgets('非弹窗 Inherited 不渲染 popupBottomBuilder', (tester) async { + await tester.pumpWidget( + _buildTestApp( + TCalendarInherited( + selected: ValueNotifier>([]), + usePopup: false, popupBottomBuilder: (_, __) => const Text('底部'), + child: TCalendar( + titleWidget: const Text('测试'), + height: 640, + ), ), ), ); - expect(tester.takeException(), isA()); expect(find.text('底部'), findsNothing); }); @@ -50,11 +70,11 @@ void main() { TCalendarInherited( selected: selected, usePopup: true, + popupBottomBuilder: (_, dates) => Text('days:${dates.length}'), child: TCalendar( titleWidget: const Text('测试'), height: 640, initialValue: selected.value, - popupBottomBuilder: (_, dates) => Text('days:${dates.length}'), ), ), ), @@ -78,11 +98,11 @@ void main() { TCalendarInherited( selected: selected, usePopup: true, + popupBottomExpanded: expanded, + popupBottomBuilder: (_, __) => const Text('底部内容'), child: TCalendar( titleWidget: const Text('测试'), height: 640, - popupBottomExpanded: expanded, - popupBottomBuilder: (_, __) => const Text('底部内容'), ), ), ), @@ -258,13 +278,12 @@ void main() { ); await tester.pumpAndSettle(); - // 点击已选中的 15 号取消 - await tester.tap(find.text('15')); + await tester.tap(find.text('20')); await tester.pump(); expect(result, isNotNull); expect(result!.length, 1); - expect(result!.first, day20); + expect(result!.first, day15); }); }); @@ -273,6 +292,8 @@ void main() { // ----------------------------------------------------------------------- group('TCalendar — 区间选择 (range)', () { testWidgets('选择 start 和 end 触发 onChange', (tester) async { + final day15 = _day(2024, 6, 15); + final day20 = _day(2024, 6, 20); final minDate = _day(2024, 6, 1); final maxDate = _day(2024, 6, 30); List? result; @@ -290,25 +311,20 @@ void main() { ); await tester.pumpAndSettle(); - // 先点 10 号作为 start - await tester.tap(find.text('10')); + await tester.tap(find.text('15')); await tester.pump(); - - expect(result, isNotNull); - expect(result!.length, 1); - expect(result!.first, _day(2024, 6, 10)); - - // 再点 20 号作为 end await tester.tap(find.text('20')); await tester.pump(); + expect(result, isNotNull); expect(result!.length, 2); - expect(result!.first, _day(2024, 6, 10)); - expect(result!.last, _day(2024, 6, 20)); + expect(result![0], day15); + expect(result![1], day20); }); testWidgets('end 在 start 之前时重置为新 start', (tester) async { - final day20 = _day(2024, 6, 20); + final day15 = _day(2024, 6, 15); + final day10 = _day(2024, 6, 10); final minDate = _day(2024, 6, 1); final maxDate = _day(2024, 6, 30); List? result; @@ -318,7 +334,6 @@ void main() { TCalendar( height: 640, type: CalendarType.range, - initialValue: [day20], minDate: minDate, maxDate: maxDate, onChange: (v) => result = v, @@ -327,13 +342,14 @@ void main() { ); await tester.pumpAndSettle(); - // 点击 10 号(在 start=20 之前)应重置 + await tester.tap(find.text('15')); + await tester.pump(); await tester.tap(find.text('10')); await tester.pump(); expect(result, isNotNull); expect(result!.length, 1); - expect(result!.first, _day(2024, 6, 10)); + expect(result!.first, day10); }); }); From 05bf03106c93064855814dc03eba52fc83e3c5d0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 03:34:33 +0000 Subject: [PATCH 25/35] [autofix.ci] apply automated fixes --- .../example/assets/api/calendar_api.md | 4 +- tdesign-site/src/calendar/README.md | 90 +++++++++++++------ 2 files changed, 64 insertions(+), 30 deletions(-) diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md index 714f5abd1..35117bafd 100644 --- a/tdesign-component/example/assets/api/calendar_api.md +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -21,8 +21,6 @@ | onCellClick | void Function(DateTime value, DateSelectType selectType, TDate tdate)? | - | 点击日期时触发 | | onChange | void Function(List value)? | - | 选中值变化时触发 | | onMonthChange | ValueChanged? | - | 月份变化时触发 | -| popupBottomBuilder | Widget Function(BuildContext context, List selectedDates)? | - | 弹窗底部自定义区域构建器,以浮层方式叠加在日历主体之上。 | -| popupBottomExpanded | ValueListenable? | - | 弹窗底部区域是否展开(响应式)。**仅在弹窗模式下生效。** | | safeAreaInset | bool | true | 是否适配底部安全区域(如 iPhone Home Indicator),默认 true | | style | TCalendarStyle? | - | 自定义样式 | | titleWidget | Widget? | - | 标题组件,可传入 Text 或自定义 Widget | @@ -33,7 +31,7 @@ | 名称 | 返回类型 | 参数 | 说明 | | --- | --- | --- | --- | -| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget? confirmBtn, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TDate tdate)? onCellClick, bool autoClose, bool draggable, Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? cellWidget, TCalendarDisplayMode displayMode, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | +| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget? confirmBtn, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TDate tdate)? onCellClick, bool autoClose, bool draggable, Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? cellWidget, TCalendarDisplayMode displayMode, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | ``` ``` diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md index 827428f5a..63e25089c 100644 --- a/tdesign-site/src/calendar/README.md +++ b/tdesign-site/src/calendar/README.md @@ -49,25 +49,38 @@ Widget _buildStyle(BuildContext context) { 15: '元宵节', }; + final customTextSelected = + ValueNotifier>([DateTime(2022, 1, 15)]); + final customBtnSelected = + ValueNotifier>([DateTime.now()]); final customCellSelected = ValueNotifier>( [DateTime.now().add(const Duration(days: 30))]); return ValueListenableBuilder( - valueListenable: customCellSelected, - builder: (context, cellValue, _) { - final cellDate = cellValue[0]; - return TCellGroup( - cells: [ + valueListenable: customTextSelected, + builder: (context, textSelected, _) { + return ValueListenableBuilder( + valueListenable: customBtnSelected, + builder: (context, btnSelected, _) { + return ValueListenableBuilder( + valueListenable: customCellSelected, + builder: (context, cellValue, _) { + final cellDate = cellValue[0]; + return TCellGroup( + cells: [ // 1. 自定义文案(cellWidget 回调自定义 cell 渲染) TCell( title: '自定义文案', arrow: true, - onClick: (cell) { + note: _formatYmd(textSelected), + onClick: (_) { TCalendar.showPopup( context, titleWidget: const Text('请选择日期'), + initialValue: textSelected, minDate: DateTime(2022, 1, 1), maxDate: DateTime(2022, 2, 15), + onConfirm: (value) => customTextSelected.value = value, cellWidget: (context, tdate, selectType) { final isSpecial = tdate.date.month == 2 && map.keys.contains(tdate.date.day); @@ -114,23 +127,25 @@ Widget _buildStyle(BuildContext context) { TCell( title: '自定义按钮', arrow: true, - onClick: (cell) { + note: _formatYmd(btnSelected), + onClick: (_) { TCalendar.showPopup( context, titleWidget: const Text('请选择日期'), - initialValue: [DateTime.now()], - confirmBtn: Padding( + initialValue: btnSelected, + confirmBtnBuilder: (onConfirm) => Padding( padding: EdgeInsets.symmetric( vertical: TTheme.of(context).spacer16), - child: const TButton( + child: TButton( theme: TButtonTheme.danger, shape: TButtonShape.round, text: 'ok', isBlock: true, size: TButtonSize.large, + onTap: onConfirm, ), ), - onConfirm: (value) => print('confirmed: $value'), + onConfirm: (value) => customBtnSelected.value = value, ); }, ), @@ -193,7 +208,11 @@ Widget _buildStyle(BuildContext context) { ); }, ), - ], + ], + ); + }, + ); + }, ); }, ); @@ -215,25 +234,38 @@ Widget _buildStyle(BuildContext context) { 15: '元宵节', }; + final customTextSelected = + ValueNotifier>([DateTime(2022, 1, 15)]); + final customBtnSelected = + ValueNotifier>([DateTime.now()]); final customCellSelected = ValueNotifier>( [DateTime.now().add(const Duration(days: 30))]); return ValueListenableBuilder( - valueListenable: customCellSelected, - builder: (context, cellValue, _) { - final cellDate = cellValue[0]; - return TCellGroup( - cells: [ + valueListenable: customTextSelected, + builder: (context, textSelected, _) { + return ValueListenableBuilder( + valueListenable: customBtnSelected, + builder: (context, btnSelected, _) { + return ValueListenableBuilder( + valueListenable: customCellSelected, + builder: (context, cellValue, _) { + final cellDate = cellValue[0]; + return TCellGroup( + cells: [ // 1. 自定义文案(cellWidget 回调自定义 cell 渲染) TCell( title: '自定义文案', arrow: true, - onClick: (cell) { + note: _formatYmd(textSelected), + onClick: (_) { TCalendar.showPopup( context, titleWidget: const Text('请选择日期'), + initialValue: textSelected, minDate: DateTime(2022, 1, 1), maxDate: DateTime(2022, 2, 15), + onConfirm: (value) => customTextSelected.value = value, cellWidget: (context, tdate, selectType) { final isSpecial = tdate.date.month == 2 && map.keys.contains(tdate.date.day); @@ -280,23 +312,25 @@ Widget _buildStyle(BuildContext context) { TCell( title: '自定义按钮', arrow: true, - onClick: (cell) { + note: _formatYmd(btnSelected), + onClick: (_) { TCalendar.showPopup( context, titleWidget: const Text('请选择日期'), - initialValue: [DateTime.now()], - confirmBtn: Padding( + initialValue: btnSelected, + confirmBtnBuilder: (onConfirm) => Padding( padding: EdgeInsets.symmetric( vertical: TTheme.of(context).spacer16), - child: const TButton( + child: TButton( theme: TButtonTheme.danger, shape: TButtonShape.round, text: 'ok', isBlock: true, size: TButtonSize.large, + onTap: onConfirm, ), ), - onConfirm: (value) => print('confirmed: $value'), + onConfirm: (value) => customBtnSelected.value = value, ); }, ), @@ -359,7 +393,11 @@ Widget _buildStyle(BuildContext context) { ); }, ), - ], + ], + ); + }, + ); + }, ); }, ); @@ -416,8 +454,6 @@ Widget _buildLunar(BuildContext context) { | onCellClick | void Function(DateTime value, DateSelectType selectType, TDate tdate)? | - | 点击日期时触发 | | onChange | void Function(List value)? | - | 选中值变化时触发 | | onMonthChange | ValueChanged? | - | 月份变化时触发 | -| popupBottomBuilder | Widget Function(BuildContext context, List selectedDates)? | - | 弹窗底部自定义区域构建器,以浮层方式叠加在日历主体之上。 | -| popupBottomExpanded | ValueListenable? | - | 弹窗底部区域是否展开(响应式)。**仅在弹窗模式下生效。** | | safeAreaInset | bool | true | 是否适配底部安全区域(如 iPhone Home Indicator),默认 true | | style | TCalendarStyle? | - | 自定义样式 | | titleWidget | Widget? | - | 标题组件,可传入 Text 或自定义 Widget | @@ -428,7 +464,7 @@ Widget _buildLunar(BuildContext context) { | 名称 | 返回类型 | 参数 | 说明 | | --- | --- | --- | --- | -| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget? confirmBtn, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TDate tdate)? onCellClick, bool autoClose, bool draggable, Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? cellWidget, TCalendarDisplayMode displayMode, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | +| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget? confirmBtn, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TDate tdate)? onCellClick, bool autoClose, bool draggable, Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? cellWidget, TCalendarDisplayMode displayMode, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | ``` ``` From 5a778655f7ec543a9fb7fcc8becd7897259f8b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Wed, 20 May 2026 11:36:15 +0800 Subject: [PATCH 26/35] =?UTF-8?q?feat(calendar):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=97=A5=E5=8E=86=E7=BB=84=E4=BB=B6=E7=94=9F=E6=88=90=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E6=B7=BB=E5=8A=A0=20TCalendarInherited=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=B9=B6=E5=AE=8C=E5=96=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tdesign-component/demo_tool/all_build.sh | 2 +- .../assets/code/calendar._buildStyle.txt | 2 +- .../example/lib/page/t_calendar_page.dart | 47 ++----------------- 3 files changed, 7 insertions(+), 44 deletions(-) diff --git a/tdesign-component/demo_tool/all_build.sh b/tdesign-component/demo_tool/all_build.sh index 4940b4d08..a47d44341 100644 --- a/tdesign-component/demo_tool/all_build.sh +++ b/tdesign-component/demo_tool/all_build.sh @@ -40,7 +40,7 @@ dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/compo # 输入 # calendar -dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/calendar" --name TCalendar,TCalendarStyle,TCalendarDataSource,TLunarInfo,TCalendarDateType --folder-name calendar --output "$PARENT_DIR/example/assets/api/" --only-api +dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/calendar" --name TCalendar,TCalendarInherited,TCalendarStyle,TCalendarDataSource,TLunarInfo,TCalendarDateType --folder-name calendar --output "$PARENT_DIR/example/assets/api/" --only-api # cascader dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/cascader" --name TMultiCascader --folder-name cascader --output "$PARENT_DIR/example/assets/api/" --only-api diff --git a/tdesign-component/example/assets/code/calendar._buildStyle.txt b/tdesign-component/example/assets/code/calendar._buildStyle.txt index 31cd453d7..fa91cfdcb 100644 --- a/tdesign-component/example/assets/code/calendar._buildStyle.txt +++ b/tdesign-component/example/assets/code/calendar._buildStyle.txt @@ -27,7 +27,7 @@ Widget _buildStyle(BuildContext context) { final cellDate = cellValue[0]; return TCellGroup( cells: [ - // 1. 自定义文案(cellWidget 回调自定义 cell 渲染) + // 1. 自定义文案(cellWidget,仅 showPopup 弹窗模式) TCell( title: '自定义文案', arrow: true, diff --git a/tdesign-component/example/lib/page/t_calendar_page.dart b/tdesign-component/example/lib/page/t_calendar_page.dart index 4ad5a0560..07f8d3aaf 100644 --- a/tdesign-component/example/lib/page/t_calendar_page.dart +++ b/tdesign-component/example/lib/page/t_calendar_page.dart @@ -91,7 +91,8 @@ class _SimpleDemo extends StatelessWidget { // ========================= 1. 单选 + 天气 ========================= /// 单选日历 + bottom 天气面板 /// -/// 演示 [TCalendar.showPopup] 的 popupBottomBuilder / popupBottomExpanded 用法: +/// 演示 [TCalendar.showPopup] 的 popupBottomBuilder / popupBottomExpanded 用法 +/// (底部区域仅弹窗模式,经 [TCalendarInherited] 注入,不可传给内嵌 [TCalendar]): /// - 选中日期后展开 bottom 区域显示天气信息 /// - 确认后回传选中值 class _SingleCalendarCell extends StatefulWidget { @@ -897,7 +898,7 @@ Widget _buildStyle(BuildContext context) { final cellDate = cellValue[0]; return TCellGroup( cells: [ - // 1. 自定义文案(cellWidget 回调自定义 cell 渲染) + // 1. 自定义文案(cellWidget,仅 showPopup 弹窗模式) TCell( title: '自定义文案', arrow: true, @@ -1050,8 +1051,7 @@ Widget _buildStyle(BuildContext context) { /// 「组件样式 - 农历日历」 /// /// 非弹窗内嵌模式,结合 [TCalendarDataSource] 展示农历信息, -/// 支持月份切换、年份/月份弹窗选择、农历信息开关。 -/// 点击日期时通过 SnackBar 显示完整的农历/节气/节日/假期信息。 +/// 支持月份切换、年份/月份弹窗选择、显示模式切换。 @Demo(group: 'calendar') Widget _buildLunar(BuildContext context) { return const _LunarCalendarDemo(); @@ -1123,44 +1123,7 @@ class _LunarCalendarDemoState extends State<_LunarCalendarDemo> { _LunarControlBar.monthKey.currentState ?.updateMonth(DateTime(month.year, month.month, 1)); }, - onCellClick: (date, selectType, tdate) { - final lunarInfo = _dataSource.getLunarInfo(date); - final solarTerm = _dataSource.getSolarTerm(date); - final festival = _dataSource.getFestival(date, lunarInfo); - final holidayInfo = _dataSource.getHolidayInfo(date); - - final buffer = StringBuffer(); - buffer.write('阳历:${date.year}年${date.month}月${date.day}日'); - - if (lunarInfo != null) { - buffer.write('\n农历:${lunarInfo.monthText}${lunarInfo.dayText}'); - } - - if (solarTerm != null && solarTerm.isNotEmpty) { - buffer.write('\n节气:$solarTerm'); - } - - if (festival != null && festival.isNotEmpty) { - buffer.write('\n节日:$festival'); - } - - if (holidayInfo != null) { - final type = holidayInfo['type'] == 'holiday' ? '假期' : '调休'; - buffer.write('\n$type:${holidayInfo['name']}'); - } - - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(buffer.toString()), - duration: const Duration(seconds: 3), - behavior: SnackBarBehavior.floating, - ), - ); - }, - onChange: (value) { - setState(() => _selected = value); - }, + onChange: (value) => setState(() => _selected = value), ), ], ); From 894e48a0430aa8099ffa972935691e90315e972a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 03:45:37 +0000 Subject: [PATCH 27/35] [autofix.ci] apply automated fixes --- .../example/assets/api/calendar_api.md | 28 ++++++++++++++++ tdesign-site/src/calendar/README.md | 32 +++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md index 35117bafd..729458123 100644 --- a/tdesign-component/example/assets/api/calendar_api.md +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -36,6 +36,34 @@ ``` ``` +### TCalendarInherited +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| child | | - | | +| confirmBtn | Widget? | - | 自定义确认按钮(静态 Widget,需自行处理点击;推荐 [confirmBtnBuilder])。 | +| confirmBtnBuilder | Widget Function(VoidCallback onConfirm)? | - | 自定义确认按钮构建器,[onConfirm] 与默认确认按钮行为一致(回传选中值并关闭弹窗)。 | +| key | | - | | +| onClose | | - | | +| onConfirm | | - | | +| popupBottomBuilder | Widget Function(BuildContext context, List selectedDates)? | - | 弹窗底部自定义区域构建器(仅弹窗模式,由 [TCalendar.showPopup] 或手动 | +| popupBottomExpanded | ValueListenable? | - | 弹窗底部区域是否展开(响应式),需配合 [popupBottomBuilder]。 | +| popupConfirmBtn | bool? | - | 是否由 [TCalendar] 渲染底部确认按钮。 | +| popupControls | bool | true | 是否由 [TCalendar] 自行渲染关闭按钮和标题行。 | +| selected | ValueNotifier> | - | 选中态的可写引用(仅供 [TCalendar] 内部更新使用)。 | +| usePopup | | true | | + + +#### 静态方法 + +| 名称 | 返回类型 | 参数 | 说明 | +| --- | --- | --- | --- | +| of | | required BuildContext context, | | + +``` +``` + ### TCalendarStyle #### 默认构造方法 diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md index 63e25089c..9c7971842 100644 --- a/tdesign-site/src/calendar/README.md +++ b/tdesign-site/src/calendar/README.md @@ -68,7 +68,7 @@ Widget _buildStyle(BuildContext context) { final cellDate = cellValue[0]; return TCellGroup( cells: [ - // 1. 自定义文案(cellWidget 回调自定义 cell 渲染) + // 1. 自定义文案(cellWidget,仅 showPopup 弹窗模式) TCell( title: '自定义文案', arrow: true, @@ -253,7 +253,7 @@ Widget _buildStyle(BuildContext context) { final cellDate = cellValue[0]; return TCellGroup( cells: [ - // 1. 自定义文案(cellWidget 回调自定义 cell 渲染) + // 1. 自定义文案(cellWidget,仅 showPopup 弹窗模式) TCell( title: '自定义文案', arrow: true, @@ -469,6 +469,34 @@ Widget _buildLunar(BuildContext context) { ``` ``` +### TCalendarInherited +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| child | | - | | +| confirmBtn | Widget? | - | 自定义确认按钮(静态 Widget,需自行处理点击;推荐 [confirmBtnBuilder])。 | +| confirmBtnBuilder | Widget Function(VoidCallback onConfirm)? | - | 自定义确认按钮构建器,[onConfirm] 与默认确认按钮行为一致(回传选中值并关闭弹窗)。 | +| key | | - | | +| onClose | | - | | +| onConfirm | | - | | +| popupBottomBuilder | Widget Function(BuildContext context, List selectedDates)? | - | 弹窗底部自定义区域构建器(仅弹窗模式,由 [TCalendar.showPopup] 或手动 | +| popupBottomExpanded | ValueListenable? | - | 弹窗底部区域是否展开(响应式),需配合 [popupBottomBuilder]。 | +| popupConfirmBtn | bool? | - | 是否由 [TCalendar] 渲染底部确认按钮。 | +| popupControls | bool | true | 是否由 [TCalendar] 自行渲染关闭按钮和标题行。 | +| selected | ValueNotifier> | - | 选中态的可写引用(仅供 [TCalendar] 内部更新使用)。 | +| usePopup | | true | | + + +#### 静态方法 + +| 名称 | 返回类型 | 参数 | 说明 | +| --- | --- | --- | --- | +| of | | required BuildContext context, | | + +``` +``` + ### TCalendarStyle #### 默认构造方法 From b3f37f52969701b5dafa78a2ac8ee8e28f56407f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Wed, 20 May 2026 15:28:40 +0800 Subject: [PATCH 28/35] =?UTF-8?q?feat(calendar):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=97=A5=E5=8E=86=E7=BB=84=E4=BB=B6=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E5=8D=95=E5=85=83=E6=A0=BC=E5=92=8C?= =?UTF-8?q?=E5=89=AF=E6=A0=87=E9=A2=98=EF=BC=8C=E9=87=8D=E6=9E=84=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tdesign-component/demo_tool/all_build.sh | 2 +- .../assets/code/calendar._buildStyle.txt | 38 +- .../lib/lunar_data_source_example.dart | 162 ++------ .../example/lib/page/t_calendar_page.dart | 76 +--- .../src/components/calendar/t_calendar.dart | 159 +++---- .../components/calendar/t_calendar_body.dart | 56 +-- .../components/calendar/t_calendar_cell.dart | 389 +++++++----------- .../calendar/t_calendar_data_source.dart | 165 +------- .../components/calendar/t_calendar_style.dart | 79 ++-- .../src/components/calendar/t_lunar_date.dart | 9 - .../test/t_calendar_lunar_test.dart | 197 +-------- 11 files changed, 364 insertions(+), 968 deletions(-) diff --git a/tdesign-component/demo_tool/all_build.sh b/tdesign-component/demo_tool/all_build.sh index a47d44341..a2fbdff45 100644 --- a/tdesign-component/demo_tool/all_build.sh +++ b/tdesign-component/demo_tool/all_build.sh @@ -40,7 +40,7 @@ dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/compo # 输入 # calendar -dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/calendar" --name TCalendar,TCalendarInherited,TCalendarStyle,TCalendarDataSource,TLunarInfo,TCalendarDateType --folder-name calendar --output "$PARENT_DIR/example/assets/api/" --only-api +dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/calendar" --name TCalendar,TCalendarInherited,TCalendarStyle,TCalendarDataSource,TCalendarCellModel,TLunarInfo --folder-name calendar --output "$PARENT_DIR/example/assets/api/" --only-api # cascader dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/cascader" --name TMultiCascader --folder-name cascader --output "$PARENT_DIR/example/assets/api/" --only-api diff --git a/tdesign-component/example/assets/code/calendar._buildStyle.txt b/tdesign-component/example/assets/code/calendar._buildStyle.txt index fa91cfdcb..054cbc8ec 100644 --- a/tdesign-component/example/assets/code/calendar._buildStyle.txt +++ b/tdesign-component/example/assets/code/calendar._buildStyle.txt @@ -27,7 +27,7 @@ Widget _buildStyle(BuildContext context) { final cellDate = cellValue[0]; return TCellGroup( cells: [ - // 1. 自定义文案(cellWidget,仅 showPopup 弹窗模式) + // 1. 自定义文案(cellBuilder,仅 showPopup 弹窗模式) TCell( title: '自定义文案', arrow: true, @@ -40,16 +40,16 @@ Widget _buildStyle(BuildContext context) { minDate: DateTime(2022, 1, 1), maxDate: DateTime(2022, 2, 15), onConfirm: (value) => customTextSelected.value = value, - cellWidget: (context, tdate, selectType) { - final isSpecial = tdate.date.month == 2 && - map.keys.contains(tdate.date.day); - final suffix = isSpecial ? '¥100' : '¥60'; - final prefix = isSpecial ? map[tdate.date.day] : null; + cellBuilder: (context, cell) { + final isSpecial = cell.date.month == 2 && + map.keys.contains(cell.date.day); + final sub = isSpecial ? '¥100' : '¥60'; + final top = isSpecial ? map[cell.date.day] : null; return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (prefix != null) - Text(prefix, + if (top != null) + Text(top, style: TextStyle( fontSize: 9, color: isSpecial @@ -57,19 +57,19 @@ Widget _buildStyle(BuildContext context) { : null, )), Text( - tdate.date.day.toString(), + cell.date.day.toString(), style: TextStyle( - color: selectType == DateSelectType.selected + color: cell.selectType == DateSelectType.selected ? TTheme.of(context).fontWhColor1 : isSpecial ? TTheme.of(context).errorColor6 : null, ), ), - Text(suffix, + Text(sub, style: TextStyle( fontSize: 9, - color: selectType == DateSelectType.selected + color: cell.selectType == DateSelectType.selected ? TTheme.of(context).fontWhColor1 : isSpecial ? TTheme.of(context).errorColor6 @@ -109,7 +109,7 @@ Widget _buildStyle(BuildContext context) { }, ), - // 3. 自定义日期单元格(cellWidget 回调) + // 3. 自定义日期单元格(cellBuilder 回调) TCell( title: '自定义日期单元格', arrow: true, @@ -121,12 +121,12 @@ Widget _buildStyle(BuildContext context) { initialValue: cellValue, cellHeight: 80, onConfirm: (value) => customCellSelected.value = value, - cellWidget: (context, tdate, selectType) { + cellBuilder: (context, cell) { final today = DateTime.now(); - final isToday = tdate.date == + final isToday = cell.date == DateTime(today.year, today.month, today.day); - if (isToday && selectType != DateSelectType.selected) { + if (isToday && cell.selectType != DateSelectType.selected) { return _CustomCellContainer( color: TTheme.of(context).brandColor4, child: const Text('今天', @@ -136,13 +136,13 @@ Widget _buildStyle(BuildContext context) { color: Colors.white)), ); } - if (selectType == DateSelectType.selected) { + if (cell.selectType == DateSelectType.selected) { return _CustomCellContainer( color: TTheme.of(context).successColor8, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('${tdate.date.day}', + Text('${cell.date.day}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -157,7 +157,7 @@ Widget _buildStyle(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('${tdate.date.day}', + Text('${cell.date.day}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold)), const Text('自定义', style: TextStyle(fontSize: 8)), diff --git a/tdesign-component/example/lib/lunar_data_source_example.dart b/tdesign-component/example/lib/lunar_data_source_example.dart index 63072e22f..1201af4bd 100644 --- a/tdesign-component/example/lib/lunar_data_source_example.dart +++ b/tdesign-component/example/lib/lunar_data_source_example.dart @@ -1,16 +1,9 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:lunar/lunar.dart'; -/// 基于 lunar 库的农历数据源实现示例 -/// -/// 使用方法: -/// TCalendar( -/// dateType: TCalendarDateType.lunar, -/// dataSource: LunarDataSourceExample(), -/// ) +/// 基于 lunar 库的农历示例:副标题由 [getSubtitle] 或 [subtitleBuilder] 接入。 class LunarDataSourceExample extends TCalendarDataSource { - /// 将数字转换为中文数字 - static String _convertToChineseNumber(int number) { + static String convertToChineseNumber(int number) { const digits = ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九']; return number .toString() @@ -19,8 +12,7 @@ class LunarDataSourceExample extends TCalendarDataSource { .join(); } - /// 获取农历月份中文名称 - static String _getLunarMonthName(int month, bool isLeapMonth) { + static String lunarMonthName(int month, bool isLeapMonth) { const months = [ '正月', '二月', @@ -39,8 +31,7 @@ class LunarDataSourceExample extends TCalendarDataSource { return isLeapMonth ? '闰$monthText' : monthText; } - /// 获取农历日期中文名称 - static String _getLunarDayName(int day) { + static String lunarDayName(int day) { const days = [ '初一', '初二', @@ -76,51 +67,56 @@ class LunarDataSourceExample extends TCalendarDataSource { return days[day - 1]; } - @override TLunarInfo? getLunarInfo(DateTime solarDate) { try { final solar = Solar.fromDate(solarDate); final lunar = solar.getLunar(); - + return TLunarInfo( year: lunar.getYear(), month: lunar.getMonth().abs(), day: lunar.getDay(), isLeapMonth: lunar.getMonth() < 0, - yearText: _convertToChineseNumber(lunar.getYear()), - monthText: _getLunarMonthName(lunar.getMonth().abs(), lunar.getMonth() < 0), - dayText: _getLunarDayName(lunar.getDay()), + yearText: convertToChineseNumber(lunar.getYear()), + monthText: lunarMonthName(lunar.getMonth().abs(), lunar.getMonth() < 0), + dayText: lunarDayName(lunar.getDay()), ); } catch (e) { - print('农历转换错误: $e'); return null; } } @override - String formatDate( - DateTime date, - TCalendarDateType type, [ - TLunarInfo? lunarInfo, - ]) { - if (type == TCalendarDateType.solar) { - return '${date.year}年${date.month}月${date.day}日'; - } else { - if (lunarInfo != null) { - return '${lunarInfo.yearText}年 ${lunarInfo.monthText}${lunarInfo.dayText}'; - } - return '${date.year}年${date.month}月${date.day}日'; + String? getSubtitle(DateTime date) { + final lunar = getLunarInfo(date); + final festival = festivalOf(date, lunar); + if (festival != null && festival.isNotEmpty) { + return festival; + } + final term = solarTermOf(date); + if (term != null && term.isNotEmpty) { + return term; } + return lunar?.dayText; } - @override - String? getSolarTerm(DateTime date) { - // 节气功能暂未实现 - // lunar 包的不同版本 API 可能不同 - // 可以使用专门的节气计算库或查表法 - - // 以下是 2026 年部分节气示例数据(仅用于演示) - final key = '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + String formatYear(int year, {bool lunar = false}) { + if (!lunar) { + return '$year年'; + } + return '${convertToChineseNumber(year)}年'; + } + + String formatMonth(int month, {bool lunar = false, bool isLeapMonth = false}) { + if (!lunar) { + return '$month月'; + } + return lunarMonthName(month, isLeapMonth); + } + + String? solarTermOf(DateTime date) { + final key = + '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; const solarTerms = { '2026-03-20': '春分', '2026-04-04': '清明', @@ -145,104 +141,18 @@ class LunarDataSourceExample extends TCalendarDataSource { return solarTerms[key]; } - @override - String? getFestival(DateTime date, [TLunarInfo? lunarInfo]) { - // 阳历节日 + String? festivalOf(DateTime date, [TLunarInfo? lunarInfo]) { if (date.month == 1 && date.day == 1) return '元旦'; if (date.month == 2 && date.day == 14) return '情人节'; - if (date.month == 3 && date.day == 8) return '妇女节'; if (date.month == 5 && date.day == 1) return '劳动节'; - if (date.month == 5 && date.day == 4) return '青年节'; - if (date.month == 6 && date.day == 1) return '儿童节'; - if (date.month == 7 && date.day == 1) return '建党节'; - if (date.month == 8 && date.day == 1) return '建军节'; - if (date.month == 9 && date.day == 10) return '教师节'; if (date.month == 10 && date.day == 1) return '国庆节'; - if (date.month == 12 && date.day == 25) return '圣诞节'; - // 农历节日 if (lunarInfo != null) { if (lunarInfo.month == 1 && lunarInfo.day == 1) return '春节'; if (lunarInfo.month == 1 && lunarInfo.day == 15) return '元宵节'; - if (lunarInfo.month == 2 && lunarInfo.day == 2) return '龙抬头'; if (lunarInfo.month == 5 && lunarInfo.day == 5) return '端午节'; - if (lunarInfo.month == 7 && lunarInfo.day == 7) return '七夕节'; - if (lunarInfo.month == 7 && lunarInfo.day == 15) return '中元节'; if (lunarInfo.month == 8 && lunarInfo.day == 15) return '中秋节'; - if (lunarInfo.month == 9 && lunarInfo.day == 9) return '重阳节'; - if (lunarInfo.month == 12 && lunarInfo.day == 8) return '腊八节'; - if (lunarInfo.month == 12 && lunarInfo.day == 23) return '小年'; - // 除夕:农历十二月最后一天 - if (lunarInfo.month == 12 && (lunarInfo.day == 29 || lunarInfo.day == 30)) { - // 需要判断是否是该月最后一天,这里简化处理 - return '除夕'; - } } - return null; } - - @override - Map? getHolidayInfo(DateTime date) { - // 2026 年法定节假日和调休安排(根据国务院发布的实际安排) - // 这里提供示例数据,实际使用时应根据每年最新公布的假期安排更新 - - final key = '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; - - // 法定节假日 - const holidays = { - // 元旦(2026年1月1-3日放假) - '2026-01-01': {'type': 'holiday', 'name': '元旦'}, - '2026-01-02': {'type': 'holiday', 'name': '元旦'}, - '2026-01-03': {'type': 'holiday', 'name': '元旦'}, - - // 春节(假设2026年1月29日-2月4日放假) - '2026-01-29': {'type': 'holiday', 'name': '春节'}, - '2026-01-30': {'type': 'holiday', 'name': '春节'}, - '2026-01-31': {'type': 'holiday', 'name': '春节'}, - '2026-02-01': {'type': 'holiday', 'name': '春节'}, - '2026-02-02': {'type': 'holiday', 'name': '春节'}, - '2026-02-03': {'type': 'holiday', 'name': '春节'}, - '2026-02-04': {'type': 'holiday', 'name': '春节'}, - - // 清明节(假设2026年4月4-6日放假) - '2026-04-04': {'type': 'holiday', 'name': '清明节'}, - '2026-04-05': {'type': 'holiday', 'name': '清明节'}, - '2026-04-06': {'type': 'holiday', 'name': '清明节'}, - - // 劳动节(假设2026年5月1-5日放假) - '2026-05-01': {'type': 'holiday', 'name': '劳动节'}, - '2026-05-02': {'type': 'holiday', 'name': '劳动节'}, - '2026-05-03': {'type': 'holiday', 'name': '劳动节'}, - '2026-05-04': {'type': 'holiday', 'name': '劳动节'}, - '2026-05-05': {'type': 'holiday', 'name': '劳动节'}, - - // 端午节(假设2026年6月25-27日放假) - '2026-06-25': {'type': 'holiday', 'name': '端午节'}, - '2026-06-26': {'type': 'holiday', 'name': '端午节'}, - '2026-06-27': {'type': 'holiday', 'name': '端午节'}, - - // 国庆节+中秋节(假设2026年10月1-8日放假) - '2026-10-01': {'type': 'holiday', 'name': '国庆节'}, - '2026-10-02': {'type': 'holiday', 'name': '国庆节'}, - '2026-10-03': {'type': 'holiday', 'name': '国庆节'}, - '2026-10-04': {'type': 'holiday', 'name': '国庆节'}, - '2026-10-05': {'type': 'holiday', 'name': '国庆节'}, - '2026-10-06': {'type': 'holiday', 'name': '中秋节'}, - '2026-10-07': {'type': 'holiday', 'name': '假期'}, - '2026-10-08': {'type': 'holiday', 'name': '假期'}, - }; - - // 调休工作日(假设数据,实际需根据国务院通知) - const workdays = { - '2026-01-04': {'type': 'workday', 'name': '补班'}, - '2026-01-24': {'type': 'workday', 'name': '补班'}, - '2026-02-07': {'type': 'workday', 'name': '补班'}, - '2026-04-26': {'type': 'workday', 'name': '补班'}, - '2026-09-27': {'type': 'workday', 'name': '补班'}, - '2026-10-10': {'type': 'workday', 'name': '补班'}, - }; - - return holidays[key] ?? workdays[key]; - } } diff --git a/tdesign-component/example/lib/page/t_calendar_page.dart b/tdesign-component/example/lib/page/t_calendar_page.dart index 07f8d3aaf..1c6c9e7de 100644 --- a/tdesign-component/example/lib/page/t_calendar_page.dart +++ b/tdesign-component/example/lib/page/t_calendar_page.dart @@ -898,7 +898,7 @@ Widget _buildStyle(BuildContext context) { final cellDate = cellValue[0]; return TCellGroup( cells: [ - // 1. 自定义文案(cellWidget,仅 showPopup 弹窗模式) + // 1. 自定义文案(cellBuilder,仅 showPopup 弹窗模式) TCell( title: '自定义文案', arrow: true, @@ -911,16 +911,16 @@ Widget _buildStyle(BuildContext context) { minDate: DateTime(2022, 1, 1), maxDate: DateTime(2022, 2, 15), onConfirm: (value) => customTextSelected.value = value, - cellWidget: (context, tdate, selectType) { - final isSpecial = tdate.date.month == 2 && - map.keys.contains(tdate.date.day); - final suffix = isSpecial ? '¥100' : '¥60'; - final prefix = isSpecial ? map[tdate.date.day] : null; + cellBuilder: (context, cell) { + final isSpecial = cell.date.month == 2 && + map.keys.contains(cell.date.day); + final sub = isSpecial ? '¥100' : '¥60'; + final top = isSpecial ? map[cell.date.day] : null; return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (prefix != null) - Text(prefix, + if (top != null) + Text(top, style: TextStyle( fontSize: 9, color: isSpecial @@ -928,19 +928,19 @@ Widget _buildStyle(BuildContext context) { : null, )), Text( - tdate.date.day.toString(), + cell.date.day.toString(), style: TextStyle( - color: selectType == DateSelectType.selected + color: cell.selectType == DateSelectType.selected ? TTheme.of(context).fontWhColor1 : isSpecial ? TTheme.of(context).errorColor6 : null, ), ), - Text(suffix, + Text(sub, style: TextStyle( fontSize: 9, - color: selectType == DateSelectType.selected + color: cell.selectType == DateSelectType.selected ? TTheme.of(context).fontWhColor1 : isSpecial ? TTheme.of(context).errorColor6 @@ -980,7 +980,7 @@ Widget _buildStyle(BuildContext context) { }, ), - // 3. 自定义日期单元格(cellWidget 回调) + // 3. 自定义日期单元格(cellBuilder 回调) TCell( title: '自定义日期单元格', arrow: true, @@ -992,12 +992,12 @@ Widget _buildStyle(BuildContext context) { initialValue: cellValue, cellHeight: 80, onConfirm: (value) => customCellSelected.value = value, - cellWidget: (context, tdate, selectType) { + cellBuilder: (context, cell) { final today = DateTime.now(); - final isToday = tdate.date == + final isToday = cell.date == DateTime(today.year, today.month, today.day); - if (isToday && selectType != DateSelectType.selected) { + if (isToday && cell.selectType != DateSelectType.selected) { return _CustomCellContainer( color: TTheme.of(context).brandColor4, child: const Text('今天', @@ -1007,13 +1007,13 @@ Widget _buildStyle(BuildContext context) { color: Colors.white)), ); } - if (selectType == DateSelectType.selected) { + if (cell.selectType == DateSelectType.selected) { return _CustomCellContainer( color: TTheme.of(context).successColor8, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('${tdate.date.day}', + Text('${cell.date.day}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -1028,7 +1028,7 @@ Widget _buildStyle(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('${tdate.date.day}', + Text('${cell.date.day}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold)), const Text('自定义', style: TextStyle(fontSize: 8)), @@ -1050,8 +1050,8 @@ Widget _buildStyle(BuildContext context) { /// 「组件样式 - 农历日历」 /// -/// 非弹窗内嵌模式,结合 [TCalendarDataSource] 展示农历信息, -/// 支持月份切换、年份/月份弹窗选择、显示模式切换。 +/// 非弹窗内嵌模式,通过 [TCalendarDataSource.getSubtitle] 展示农历副标题, +/// 支持月份切换、年份/月份弹窗选择。 @Demo(group: 'calendar') Widget _buildLunar(BuildContext context) { return const _LunarCalendarDemo(); @@ -1078,21 +1078,13 @@ class _LunarCalendarDemoState extends State<_LunarCalendarDemo> { static final DateTime _maxDate = DateTime(2030, 12, 31); DateTime? _anchorDate; - late TCalendarDisplayMode _displayMode; List _selected = [DateTime.now()]; - @override - void initState() { - super.initState(); - _displayMode = TCalendarDisplayMode.solarWithLunar; - } - @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ - // ---- 控制栏(独立 Widget,onMonthChange 只更新它自己) ---- _LunarControlBar( key: _LunarControlBar.monthKey, dataSource: _dataSource, @@ -1103,12 +1095,8 @@ class _LunarCalendarDemoState extends State<_LunarCalendarDemo> { _anchorDate = anchor; }); }, - onModeChanged: (mode) { - setState(() => _displayMode = mode); - }, ), - // ---- 内嵌日历 ---- TCalendar( type: CalendarType.single, minDate: _minDate, @@ -1116,7 +1104,6 @@ class _LunarCalendarDemoState extends State<_LunarCalendarDemo> { initialValue: _selected, anchorDate: _anchorDate, animateTo: true, - displayMode: _displayMode, dataSource: _dataSource, onMonthChange: (month) { // 只通知控制栏更新显示,不 setState 本身 → 日历不重建 @@ -1141,14 +1128,12 @@ class _LunarControlBar extends StatefulWidget { required this.minDate, required this.maxDate, required this.onNavigate, - required this.onModeChanged, }); final LunarDataSourceExample dataSource; final DateTime minDate; final DateTime maxDate; final ValueChanged onNavigate; - final ValueChanged onModeChanged; /// 全局 Key,供父 Widget 通过 currentState.updateMonth() 同步月份 static final GlobalKey<_LunarControlBarState> monthKey = @@ -1160,14 +1145,12 @@ class _LunarControlBar extends StatefulWidget { class _LunarControlBarState extends State<_LunarControlBar> { late DateTime _currentMonth; - late TCalendarDisplayMode _mode; @override void initState() { super.initState(); final now = DateTime.now(); _currentMonth = _clampMonth(DateTime(now.year, now.month, 1)); - _mode = TCalendarDisplayMode.solarWithLunar; } /// 将任意 (year, month) clamp 到 [minDate, maxDate] 区间内。 @@ -1411,23 +1394,6 @@ class _LunarControlBarState extends State<_LunarControlBar> { _currentMonth.year, _currentMonth.month + 1, 1)) : null, ), - const SizedBox(width: 8), - Text('农历', style: TextStyle(fontSize: 12, color: Colors.grey.shade700)), - const SizedBox(width: 4), - TSwitch( - size: TSwitchSize.small, - isOn: _mode == TCalendarDisplayMode.solarWithLunar || - _mode == TCalendarDisplayMode.lunar, - onChanged: (v) { - final newMode = v - ? TCalendarDisplayMode.solarWithLunar - : TCalendarDisplayMode.solar; - setState(() => _mode = newMode); - widget.onModeChanged(newMode); - // 已自行处理状态,返回 true 阻止 TSwitch 内部刷新(避免双源冲突) - return true; - }, - ), ], ), ], diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart index d84518565..901df442d 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart @@ -7,7 +7,14 @@ import 't_calendar_body.dart'; import 't_calendar_cell.dart'; import 't_calendar_header.dart'; -export 't_calendar_cell.dart' show TDate, DateSelectTypeNotifier; +export 't_calendar_cell.dart' + show + TCalendarCellModel, + DateSelectTypeNotifier, + DateSelectType, + TCalendarSubtitleContext, + TCalendarSubtitleBuilder, + TCalendarCellBuilder; export 't_calendar_data_source.dart'; export 't_calendar_style.dart'; export 't_lunar_date.dart'; @@ -102,21 +109,6 @@ enum CalendarType { range, } -/// 日期在日历中的选中状态 -enum DateSelectType { selected, disabled, start, centre, end, empty } - -/// 日历显示模式,控制日期单元格的主/副文本内容 -enum TCalendarDisplayMode { - /// 纯阳历:主文本显示阳历日期数字,无副文本 - solar, - - /// 阳历 + 农历副标题:主文本显示阳历日期数字,副文本显示农历(如"初七") - solarWithLunar, - - /// 农历为主:主文本显示农历(如"初七"),副文本显示阳历日期数字 - lunar, -} - /// 日历组件 class TCalendar extends StatefulWidget { const TCalendar({ @@ -136,10 +128,10 @@ class TCalendar extends StatefulWidget { this.monthTitleHeight = 22, this.monthTitleBuilder, this.animateTo = false, - this.cellWidget, + this.cellBuilder, + this.subtitleBuilder, this.onMonthChange, this.anchorDate, - this.displayMode = TCalendarDisplayMode.solar, this.dataSource, }) : super(key: key); @@ -185,7 +177,7 @@ class TCalendar extends StatefulWidget { final void Function( DateTime value, DateSelectType selectType, - TDate tdate, + TCalendarCellModel cell, )? onCellClick; /// 月份变化时触发 @@ -206,26 +198,16 @@ class TCalendar extends StatefulWidget { /// 滚动到选中日期/锚点日期所在月份时是否使用动画,默认 false final bool animateTo; - /// 自定义日期单元格组件 - final Widget? Function( - BuildContext context, - TDate tdate, - DateSelectType selectType, - )? cellWidget; + /// 整格自定义;设置后不再使用默认主区/副标题布局。 + final TCalendarCellBuilder? cellBuilder; + + /// 副标题完全自定义;未设置时可使用 [dataSource.getSubtitle]。 + final TCalendarSubtitleBuilder? subtitleBuilder; /// 锚点日期,弹出时自动滚动到该日期所在月份。 - /// 传入 [DateTime] 对象,如 `DateTime(2025, 6, 15)`。 final DateTime? anchorDate; - /// 日历显示模式,控制日期单元格的主/副文本内容: - /// - [TCalendarDisplayMode.solar]:纯阳历,主文本显示阳历日期数字 - /// - [TCalendarDisplayMode.solarWithLunar]:阳历 + 农历副标题 - /// - [TCalendarDisplayMode.lunar]:农历为主文本,阳历为副文本 - final TCalendarDisplayMode displayMode; - - /// 外部数据源,用于提供农历转换等功能。 - /// 当 [displayMode] 为 [TCalendarDisplayMode.solarWithLunar] 或 - /// [TCalendarDisplayMode.lunar] 时必须提供。 + /// 可选数据源,提供副标题字符串(无 [subtitleBuilder] 时生效)。 final TCalendarDataSource? dataSource; List? get _value => initialValue?.map((e) { @@ -329,8 +311,11 @@ class TCalendar extends StatefulWidget { VoidCallback? onClose, /// 点击日期时触发 - void Function(DateTime value, DateSelectType selectType, TDate tdate)? - onCellClick, + void Function( + DateTime value, + DateSelectType selectType, + TCalendarCellModel cell, + )? onCellClick, /// 点击遮罩或物理返回是否关闭 bool autoClose = true, @@ -338,17 +323,8 @@ class TCalendar extends StatefulWidget { /// 面板是否可拖动 bool draggable = false, - /// 自定义日期单元格组件 - Widget? Function( - BuildContext context, - TDate tdate, - DateSelectType selectType, - )? cellWidget, - - /// 日历显示模式 - TCalendarDisplayMode displayMode = TCalendarDisplayMode.solar, - - /// 外部数据源,用于提供农历转换等功能 + TCalendarCellBuilder? cellBuilder, + TCalendarSubtitleBuilder? subtitleBuilder, TCalendarDataSource? dataSource, /// 月份变化时触发 @@ -434,8 +410,8 @@ class TCalendar extends StatefulWidget { cellHeight: cellHeight, style: style, onCellClick: onCellClick, - cellWidget: cellWidget, - displayMode: displayMode, + cellBuilder: cellBuilder, + subtitleBuilder: subtitleBuilder, dataSource: dataSource, onMonthChange: onMonthChange, monthTitleBuilder: monthTitleBuilder, @@ -482,12 +458,10 @@ class _TCalendarState extends State { /// 时直接 setType(empty) 即可。引用会随 body 缓存重生成(cleanup 后再滚回 /// 该月)被 [_handleTDateGenerated] 覆盖为新实例,不会出现"指向已 detach /// 的 TDate"导致视觉残留。 - TDate? _selectedSingleRef; + TCalendarCellModel? _selectedSingleRef; - /// multiple 模式下当前所有选中的 TDate 引用,按日期键。 - /// - /// 点击切换时直接查表决定 select/empty,避免遍历可见月份缓存。 - final Map _selectedMultipleRefs = {}; + /// multiple 模式下当前所有选中的单元格引用,按日期键。 + final Map _selectedMultipleRefs = {}; // bottom 展开时日历主体上移的距离,露出 bottom 顶部"把手"区域。 static const double _bottomPeekHeight = 30.0; @@ -498,21 +472,6 @@ class _TCalendarState extends State { bool _initializedSelected = false; - /// 解析 displayMode 为旧的 dateType 和 showLunarInfo - TCalendarDateType get _dateType { - switch (widget.displayMode) { - case TCalendarDisplayMode.solar: - case TCalendarDisplayMode.solarWithLunar: - return TCalendarDateType.solar; - case TCalendarDisplayMode.lunar: - return TCalendarDateType.lunar; - } - } - - bool get _showLunarInfo => - widget.displayMode == TCalendarDisplayMode.solarWithLunar || - widget.displayMode == TCalendarDisplayMode.lunar; - @override void didChangeDependencies() { super.didChangeDependencies(); @@ -721,22 +680,23 @@ class _TCalendarState extends State { monthTitleBuilder: widget.monthTitleBuilder, animateTo: widget.animateTo, onMonthChange: widget.onMonthChange, - dateType: _dateType, - dataSource: widget.dataSource, - onTDateGenerated: _handleTDateGenerated, + onCellGenerated: _handleCellGenerated, onCacheInvalidated: _handleCacheInvalidated, - builder: (date, dateList, rowIndex, colIndex) { + builder: (cell, dateList, rowIndex, colIndex) { return TCalendarCell( height: _getEffectiveCellHeight(), - tdate: date, + cell: cell, padding: verticalGap / 2, onTap: _handleCellTap, dateList: dateList, rowIndex: rowIndex, colIndex: colIndex, - cellWidget: widget.cellWidget, - dateType: _dateType, - showLunarInfo: _showLunarInfo, + cellBuilder: widget.cellBuilder, + subtitleBuilder: widget.subtitleBuilder, + dataSource: widget.dataSource, + dayStyle: _style.dayStyle, + todayDayStyle: _style.todayDayStyle, + subtitleStyle: _style.subtitleStyle, ); }, ); @@ -748,21 +708,21 @@ class _TCalendarState extends State { /// single:每月最多一个 selected,遇到即覆盖 _selectedSingleRef。 /// multiple:把当月所有 selected 的引用按 date 写入 map。 /// range:本身走 widget.value 重建路径,不需要登记。 - void _handleTDateGenerated(DateTime monthDate, List tdates) { + void _handleCellGenerated(DateTime monthDate, List cells) { if (widget.type == CalendarType.range) { return; } - for (final tdate in tdates) { - if (tdate == null) { + for (final cell in cells) { + if (cell == null) { continue; } - if (tdate.typeNotifier.value != DateSelectType.selected) { + if (cell.typeNotifier.value != DateSelectType.selected) { continue; } if (widget.type == CalendarType.single) { - _selectedSingleRef = tdate; + _selectedSingleRef = cell; } else if (widget.type == CalendarType.multiple) { - _selectedMultipleRefs[tdate.date] = tdate; + _selectedMultipleRefs[cell.date] = cell; } } } @@ -781,27 +741,26 @@ class _TCalendarState extends State { /// - single:切换 _selectedSingleRef,旧引用置 empty、新引用置 selected /// - multiple:根据 _selectedMultipleRefs 切换该日期的选中态 /// - range:交由 [_resolveRangeSelection] 决策后走 setState 重建(保持原有路径) - void _handleCellTap(TDate tdate) { - final selectType = tdate.typeNotifier.value; - final curDate = tdate.date; + void _handleCellTap(TCalendarCellModel cell) { + final selectType = cell.typeNotifier.value; + final curDate = cell.date; if (selectType == DateSelectType.disabled) { - widget.onCellClick?.call(curDate, selectType, tdate); + widget.onCellClick?.call(curDate, selectType, cell); return; } switch (widget.type) { case CalendarType.single: - // 已经是当前选中:仅触发 onCellClick,不重复 onChange - if (identical(_selectedSingleRef, tdate)) { - widget.onCellClick?.call(curDate, tdate.typeNotifier.value, tdate); + if (identical(_selectedSingleRef, cell)) { + widget.onCellClick?.call(curDate, cell.typeNotifier.value, cell); return; } _selectedSingleRef?.typeNotifier.setType(DateSelectType.empty); - tdate.typeNotifier.setType(DateSelectType.selected); - _selectedSingleRef = tdate; + cell.typeNotifier.setType(DateSelectType.selected); + _selectedSingleRef = cell; _emitSelection([curDate], rebuild: false); - widget.onCellClick?.call(curDate, tdate.typeNotifier.value, tdate); + widget.onCellClick?.call(curDate, cell.typeNotifier.value, cell); break; case CalendarType.multiple: final existing = _selectedMultipleRefs[curDate]; @@ -810,24 +769,20 @@ class _TCalendarState extends State { existing.typeNotifier.setType(DateSelectType.empty); _selectedMultipleRefs.remove(curDate); } else { - tdate.typeNotifier.setType(DateSelectType.selected); - _selectedMultipleRefs[curDate] = tdate; + cell.typeNotifier.setType(DateSelectType.selected); + _selectedMultipleRefs[curDate] = cell; } nextValue = _selectedMultipleRefs.keys.toList()..sort(); _emitSelection(nextValue, rebuild: false); - widget.onCellClick?.call(curDate, tdate.typeNotifier.value, tdate); + widget.onCellClick?.call(curDate, cell.typeNotifier.value, cell); break; case CalendarType.range: - // range 仍走老路径:state 决策 start/end,刷新 value,触发 body 重建。 final resolved = _resolveRangeSelection([curDate]); _emitSelection(resolved, rebuild: true); - // 上抛点击时已根据 resolved 推导出本次的语义类型(start / end), - // 这样调用方无需等到 body 重建后再读 typeNotifier,可直接用于 - // 切换关联 UI(如时间选择器 Tab)。 final reportedType = resolved.length >= 2 && resolved[1] == curDate ? DateSelectType.end : DateSelectType.start; - widget.onCellClick?.call(curDate, reportedType, tdate); + widget.onCellClick?.call(curDate, reportedType, cell); break; } } diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart index 67b4893f5..e5f7c88f1 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart @@ -23,9 +23,7 @@ class TCalendarBody extends StatefulWidget { required this.animateTo, this.onMonthChange, this.anchorDate, - this.dateType = TCalendarDateType.solar, - this.dataSource, - this.onTDateGenerated, + this.onCellGenerated, this.onCacheInvalidated, }) : super(key: key); @@ -36,8 +34,8 @@ class TCalendarBody extends StatefulWidget { final DateTime? anchorDate; final int firstDayOfWeek; final Widget Function( - TDate? date, - List dateList, + TCalendarCellModel? cell, + List dateList, int rowIndex, int colIndex, ) builder; @@ -53,19 +51,11 @@ class TCalendarBody extends StatefulWidget { final double cellHeight; final bool animateTo; final ValueChanged? onMonthChange; - final TCalendarDateType dateType; - final TCalendarDataSource? dataSource; + /// 在每个月份的单元格列表新生成时回调,便于上层登记选中引用。 + final void Function(DateTime monthDate, List cells)? + onCellGenerated; - /// 在每个月份的 TDate 列表新生成时回调,便于上层把 selected/start/end 等 - /// 状态的 TDate 引用登记到自己的"权威选中映射"。 - /// - /// 注意:每次 _data 缺失某月时都会生成新 TDate 实例并触发该回调;上层 - /// 应当以"按需覆盖"语义处理(例如同 date 的旧引用直接被新引用替换)。 - final void Function(DateTime monthDate, List tdates)? - onTDateGenerated; - - /// 当 `_data` 整体被清空(例如 minDate/maxDate 变化或 range 选择变更)时回调, - /// 上层据此清空"权威选中映射",避免悬挂指向已被 GC 的旧 TDate 实例。 + /// 当 `_data` 整体被清空时回调,上层清空选中映射。 final VoidCallback? onCacheInvalidated; @override @@ -75,7 +65,7 @@ class TCalendarBody extends StatefulWidget { class _TCalendarBodyState extends State { late final ScrollController _scrollController; int? _lastNotifiedMonthKey; - final _data = >{}; + final _data = >{}; final _monthHeight = {}; late List _months; late DateTime _min; @@ -267,7 +257,7 @@ class _TCalendarBodyState extends State { if (!_data.containsKey(monthDate)) { final tdates = _getDaysInMonth(monthDate, _min, _max); _data[monthDate] = tdates; - widget.onTDateGenerated?.call(monthDate, tdates); + widget.onCellGenerated?.call(monthDate, tdates); } } } @@ -303,7 +293,7 @@ class _TCalendarBodyState extends State { final monthDateText = '$monthYear $monthMonth'; // 只读:build 不写状态。命中缓存直接用,未命中走纯函数计算并安排 // 在下一帧补写缓存(postFrameCallback),避免 build 阶段副作用。 - List monthData; + List monthData; final cached = _data[monthDate]; if (cached != null) { monthData = cached; @@ -317,7 +307,7 @@ class _TCalendarBodyState extends State { // 注册回调也只在真正写入这条新数据时触发,避免重复登记。 if (!_data.containsKey(monthDate)) { _data[monthDate] = monthData; - widget.onTDateGenerated?.call(monthDate, monthData); + widget.onCellGenerated?.call(monthDate, monthData); } }); } @@ -495,9 +485,10 @@ class _TCalendarBodyState extends State { return months; } - List _getDaysInMonth(DateTime curDate, DateTime min, DateTime max) { - final daysInMonth = - List.generate(_getPreOffset(curDate), (index) => null); + List _getDaysInMonth( + DateTime curDate, DateTime min, DateTime max) { + final daysInMonth = List.generate( + _getPreOffset(curDate), (index) => null); final daysInMonthCount = DateTime(curDate.year, curDate.month + 1, 0) .day; // 获取下个月的第一天的前一天,即当前月的最后一天 for (var day = 1; day <= daysInMonthCount; day++) { @@ -532,25 +523,10 @@ class _TCalendarBodyState extends State { } } } - // 获取农历信息 - TLunarInfo? lunarInfo; - String? solarTerm; - String? festival; - Map? holidayInfo; - if (widget.dataSource != null) { - lunarInfo = widget.dataSource!.getLunarInfo(date); - solarTerm = widget.dataSource!.getSolarTerm(date); - festival = widget.dataSource!.getFestival(date, lunarInfo); - holidayInfo = widget.dataSource!.getHolidayInfo(date); - } - daysInMonth.add(TDate( + daysInMonth.add(TCalendarCellModel( date: date, typeNotifier: DateSelectTypeNotifier(selectType), isLastDayOfMonth: daysInMonthCount == day, - lunarInfo: lunarInfo, - solarTerm: solarTerm, - festival: festival, - holidayInfo: holidayInfo, )); } var sufOffset = 7 - daysInMonth.length % 7; diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart b/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart index 9f68d2e87..8b3cd5558 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar_cell.dart @@ -1,91 +1,153 @@ import 'package:flutter/material.dart'; import '../../../tdesign_flutter.dart'; import '../../util/iterable_ext.dart'; +import 't_calendar_data_source.dart'; +import 't_calendar_style.dart'; + +/// 日期在日历格中的选中/展示状态 +enum DateSelectType { selected, disabled, start, centre, end, empty } + +/// 副标题构建上下文 +class TCalendarSubtitleContext { + const TCalendarSubtitleContext({ + required this.date, + required this.selectType, + }); + + final DateTime date; + final DateSelectType selectType; +} + +/// 副标题完全自定义 +typedef TCalendarSubtitleBuilder = Widget? Function( + BuildContext context, + TCalendarSubtitleContext subtitleContext, +); + +/// 整格自定义(主区 + 副标题均由接入方绘制) +typedef TCalendarCellBuilder = Widget? Function( + BuildContext context, + TCalendarCellModel cell, +); + +/// 单个日期格数据(只读,选中态通过 [typeNotifier] 更新) +class TCalendarCellModel { + TCalendarCellModel({ + required this.date, + required this.typeNotifier, + required this.isLastDayOfMonth, + }); + + final DateTime date; + final DateSelectTypeNotifier typeNotifier; + final bool isLastDayOfMonth; + + DateSelectType get selectType => typeNotifier.value; +} + +class DateSelectTypeNotifier extends ChangeNotifier { + DateSelectType value = DateSelectType.empty; + + DateSelectTypeNotifier(DateSelectType selectType) { + value = selectType; + } + + void setType(DateSelectType type) { + value = type; + notifyListeners(); + } +} class TCalendarCell extends StatefulWidget { const TCalendarCell({ Key? key, - this.tdate, + this.cell, this.onTap, required this.height, required this.padding, required this.rowIndex, required this.colIndex, required this.dateList, - this.cellWidget, - this.dateType = TCalendarDateType.solar, - this.showLunarInfo = false, + this.cellBuilder, + this.subtitleBuilder, + this.dataSource, + this.dayStyle, + this.todayDayStyle, + this.subtitleStyle, }) : super(key: key); - final TDate? tdate; + final TCalendarCellModel? cell; - /// 点击回调。cell 不再负责任何选中态决策,只把"被点击的这一格"上抛给 - /// 上层 state,由其结合 [CalendarType] 决定如何更新选中。 - /// - /// 当点击 disabled cell 时同样会回调(state 内部按需要分流到 onCellClick)。 - final void Function(TDate tdate)? onTap; + final void Function(TCalendarCellModel cell)? onTap; final double height; final double padding; final int rowIndex; final int colIndex; - final List dateList; - final Widget? Function( - BuildContext context, - TDate tdate, - DateSelectType selectType, - )? cellWidget; - final TCalendarDateType dateType; - final bool showLunarInfo; + final List dateList; + final TCalendarCellBuilder? cellBuilder; + final TCalendarSubtitleBuilder? subtitleBuilder; + final TCalendarDataSource? dataSource; + final TextStyle? dayStyle; + final TextStyle? todayDayStyle; + final TextStyle? subtitleStyle; @override - _TCalendarCellState createState() => _TCalendarCellState(); + State createState() => _TCalendarCellState(); } class _TCalendarCellState extends State { - var isToday = false; - var positionOffset = 0; + var _isToday = false; + var _positionOffset = 0; @override void initState() { super.initState(); - isToday = _isToday(); - widget.tdate?.typeNotifier.addListener(_cellTypeChange); + _isToday = _checkIsToday(); + widget.cell?.typeNotifier.addListener(_onSelectTypeChange); } @override void didUpdateWidget(TCalendarCell oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.tdate != oldWidget.tdate) { - isToday = _isToday(); - oldWidget.tdate?.typeNotifier.removeListener(_cellTypeChange); - widget.tdate?.typeNotifier.addListener(_cellTypeChange); + if (widget.cell != oldWidget.cell) { + _isToday = _checkIsToday(); + oldWidget.cell?.typeNotifier.removeListener(_onSelectTypeChange); + widget.cell?.typeNotifier.addListener(_onSelectTypeChange); } } @override void dispose() { - widget.tdate?.typeNotifier.removeListener(_cellTypeChange); + widget.cell?.typeNotifier.removeListener(_onSelectTypeChange); super.dispose(); } + bool _checkIsToday() { + final today = DateTime.now(); + return widget.cell?.date == + DateTime(today.year, today.month, today.day); + } + @override Widget build(BuildContext context) { - final tdate = widget.tdate; - if (tdate == null) { + final cell = widget.cell; + if (cell == null) { return const SizedBox.shrink(); } - final cellStyle = TCalendarStyle.cellStyle(context, tdate._type); - final decoration = tdate.decoration ?? cellStyle.cellDecoration; - final positionColor = _getColor(cellStyle, decoration); - // 新增自定义cell内容判断逻辑 - final content = widget.cellWidget?.call(context, tdate, tdate._type) ?? - _buildDefaultCell(context, tdate, cellStyle); + final themedStyle = + TCalendarStyle.forSelectType(context, cell.selectType); + final decoration = + themedStyle.cellDecoration; + final positionColor = _rangeBridgeColor(themedStyle, decoration); + + final content = widget.cellBuilder?.call(context, cell) ?? + _buildDefaultCell(context, cell, themedStyle); return GestureDetector( behavior: HitTestBehavior.translucent, - onTap: _cellTap, + onTap: () => widget.onTap?.call(cell), child: Stack( clipBehavior: Clip.none, children: [ @@ -94,14 +156,14 @@ class _TCalendarCellState extends State { width: double.infinity, height: widget.height, decoration: decoration, - child: content, // 使用自定义内容 + child: content, ), ), if (widget.colIndex < 6) Positioned( - right: -widget.padding - positionOffset, + right: -widget.padding - _positionOffset, child: Container( - width: widget.padding + 2 * positionOffset, + width: widget.padding + 2 * _positionOffset, height: widget.height, color: positionColor, ), @@ -111,224 +173,93 @@ class _TCalendarCellState extends State { ); } - void _cellTap() { - final tdate = widget.tdate; - if (tdate == null) { - return; - } - // 三种模式统一:cell 不再做任何决策,只把被点击的 TDate 上抛。 - // 由 [_TCalendarState] 结合 widget.type / 当前选中映射 / value 决定如何更新。 - widget.onTap?.call(tdate); - } - - void _cellTypeChange() { + void _onSelectTypeChange() { setState(() {}); } - Color? _getColor(TCalendarStyle cellStyle, BoxDecoration? decoration) { - positionOffset = 0; + Color? _rangeBridgeColor( + TCalendarStyle cellStyle, BoxDecoration? decoration) { + _positionOffset = 0; final next = _nextDay(); - if (widget.tdate?._type == DateSelectType.start) { - if (widget.tdate?.isLastDayOfMonth == true) { + if (widget.cell?.selectType == DateSelectType.start) { + if (widget.cell?.isLastDayOfMonth == true) { return null; } - if (next?._type == DateSelectType.end) { - positionOffset = 1; + if (next?.selectType == DateSelectType.end) { + _positionOffset = 1; return decoration?.color; } - if (next?._type == DateSelectType.centre) { + if (next?.selectType == DateSelectType.centre) { return cellStyle.centreColor; } } - if (widget.tdate?._type == DateSelectType.centre) { + if (widget.cell?.selectType == 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; + TCalendarCellModel? _nextDay([int offset = 1]) { + final index = widget.rowIndex * 7 + widget.colIndex + offset; + return widget.dateList.getOrNull(index); } - bool _isToday() { - final today = DateTime.now(); - return widget.tdate?.date == - DateTime(today.year, today.month, today.day); - } - - /// 构建默认单元格内容 Widget _buildDefaultCell( - BuildContext context, TDate tdate, TCalendarStyle cellStyle) { - // 根据 dateType 和 showLunarInfo 决定显示内容 - var mainText = widget.tdate!.date.day.toString(); - String? subText; - - if (widget.dateType == TCalendarDateType.lunar && tdate.lunarInfo != null) { - // 农历模式:主文本显示农历,副文本显示阳历日期 - mainText = tdate.lunarInfo!.dayText; - subText = widget.tdate!.date.day.toString(); - } else if (widget.dateType == TCalendarDateType.solar && - widget.showLunarInfo) { - // 阳历模式+显示农历信息 - mainText = widget.tdate!.date.day.toString(); - - // 优先级:节日 > 节气 > 农历日期 - if (tdate.festival != null && tdate.festival!.isNotEmpty) { - subText = tdate.festival; - } else if (tdate.solarTerm != null && tdate.solarTerm!.isNotEmpty) { - subText = tdate.solarTerm; - } else if (tdate.lunarInfo != null) { - subText = tdate.lunarInfo!.dayText; - } - } + BuildContext context, + TCalendarCellModel cell, + TCalendarStyle cellStyle, + ) { + final dayText = cell.date.day.toString(); + final dayTextStyle = (_isToday ? cellStyle.todayDayStyle : null) ?? + widget.todayDayStyle ?? + cellStyle.dayStyle ?? + widget.dayStyle; + + final subtitle = _buildSubtitle(context, cell, cellStyle); return Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisAlignment: MainAxisAlignment.center, children: [ - // prefix 区域 - 不强制高度 - if (tdate.prefix != null || tdate.prefixWidget != null) - SizedBox( - height: 12, - child: tdate.prefixWidget ?? - TText( - tdate.prefix ?? '', - style: tdate.prefixStyle ?? cellStyle.cellPrefixStyle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - // 主内容区域 - 自适应高度 - Expanded( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TText( - forceVerticalCenter: subText == null, - mainText, - style: (isToday ? cellStyle.todayStyle : null) ?? - tdate.style ?? - cellStyle.cellStyle, - ), - if (subText != null) - Padding( - padding: const EdgeInsets.only(top: 2), - child: TText( - subText, - style: cellStyle.cellSuffixStyle?.copyWith(fontSize: 9), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), + TText( + dayText, + forceVerticalCenter: subtitle == null, + style: dayTextStyle, ), - // suffix 区域 - 不强制高度 - if (tdate.suffix != null || tdate.suffixWidget != null) - SizedBox( - height: 12, - child: tdate.suffixWidget ?? - TText( - tdate.suffix ?? '', - style: tdate.suffixStyle ?? cellStyle.cellSuffixStyle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + if (subtitle != null) + Padding( + padding: const EdgeInsets.only(top: 2), + child: subtitle, ), ], ); } -} - -/// 时间对象 -/// -/// 不可变数据载体(除 [typeNotifier] 外所有字段均为 final)。 -/// -/// 选中类型的变化通过 [typeNotifier] 通知监听者;其它视觉字段([prefix] / -/// [suffix] / [style] / [decoration] 等)请在构造时传入,不要构造后再 mutate -/// ——这些字段不发出变更通知,cell 也不会响应运行时修改。 -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, - this.lunarInfo, - this.solarTerm, - this.festival, - this.holidayInfo, - }); - - /// 时间对象 - final DateTime date; - - /// 日期类型 - final DateSelectTypeNotifier typeNotifier; - - /// 日期前面的字符串 - final String? prefix; - - /// 日期前面的字符串的样式 - final TextStyle? prefixStyle; - - /// 日期前面的组件,优先级高于[prefix] - final Widget? prefixWidget; - - /// 日期后面的字符串 - final String? suffix; - - /// 日期后面的字符串的样式 - final TextStyle? suffixStyle; - - /// 日期后面的组件,优先级高于[suffix] - final Widget? suffixWidget; - - /// 日期样式 - final TextStyle? style; - /// 日期Decoration - final BoxDecoration? decoration; - - /// 是否是当月最后一天 - final bool isLastDayOfMonth; - - /// 农历信息 - final TLunarInfo? lunarInfo; - - /// 节气信息(如"春分"、"立夏") - final String? solarTerm; - - /// 节日信息(如"春节"、"中秋节") - final String? festival; - - /// 假期信息(包含类型和名称) - /// type: 'holiday' 或 'workday' - /// name: 假期名称 - final Map? holidayInfo; - - DateSelectType get _type => typeNotifier.value; -} - -class DateSelectTypeNotifier extends ChangeNotifier { - DateSelectType value = DateSelectType.empty; + Widget? _buildSubtitle( + BuildContext context, + TCalendarCellModel cell, + TCalendarStyle cellStyle, + ) { + if (widget.subtitleBuilder != null) { + return widget.subtitleBuilder!( + context, + TCalendarSubtitleContext( + date: cell.date, + selectType: cell.selectType, + ), + ); + } - DateSelectTypeNotifier(DateSelectType selectType) { - value = selectType; - } + final text = widget.dataSource?.getSubtitle(cell.date); + if (text == null || text.isEmpty) { + return null; + } - void setType(DateSelectType type) { - value = type; - notifyListeners(); + return TText( + text, + style: cellStyle.subtitleStyle ?? + widget.subtitleStyle?.copyWith(fontSize: 9), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); } } diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_data_source.dart b/tdesign-component/lib/src/components/calendar/t_calendar_data_source.dart index 9dece2a36..3a8316e9f 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar_data_source.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar_data_source.dart @@ -1,161 +1,8 @@ -import 't_lunar_date.dart'; - -/// 日历数据源接口 -/// -/// 开发者需要实现此接口来提供农历转换能力。 -/// 组件内部不包含农历算法和数据,完全依赖外部实现。 +/// 日历可选数据源:仅提供副标题文案(无 [subtitleBuilder] 时使用)。 +/// +/// 农历、节气、节日等均由接入方在 [TCalendar.subtitleBuilder] 或 +/// [getSubtitle] 中自行处理;组件主区默认只渲染阳历日数字。 abstract class TCalendarDataSource { - /// 获取指定阳历日期的农历信息 - /// - /// [solarDate] 阳历日期 - /// - /// 返回 null 表示不显示农历信息 - TLunarInfo? getLunarInfo(DateTime solarDate); - - /// 格式化日期文本 - /// - /// [date] 阳历日期 - /// [type] 日历类型 - /// [lunarInfo] 农历信息(可选) - /// - /// 返回格式化后的日期字符串 - String formatDate( - DateTime date, - TCalendarDateType type, [ - TLunarInfo? lunarInfo, - ]); - - /// 获取节气信息(可选实现) - /// - /// [date] 阳历日期 - /// - /// 返回节气名称,如"春分"、"秋分"等,无节气则返回 null - String? getSolarTerm(DateTime date) => null; - - /// 获取节日信息(可选实现) - /// - /// [date] 阳历日期 - /// [lunarInfo] 农历信息(可选) - /// - /// 返回节日名称,如"春节"、"中秋节"等,无节日则返回 null - String? getFestival(DateTime date, [TLunarInfo? lunarInfo]) => null; - - /// 获取假期信息(可选实现) - /// - /// [date] 阳历日期 - /// - /// 返回假期类型和名称: - /// - 'holiday': 法定节假日/公共假期(如"国庆节") - /// - 'workday': 调休工作日(如"补班") - /// - null: 正常日期 - /// - /// 示例返回值: - /// - {'type': 'holiday', 'name': '国庆节'} - /// - {'type': 'workday', 'name': '补班'} - /// - null - Map? getHolidayInfo(DateTime date) => null; - - /// 格式化年份文本 - /// - /// [year] 年份 - /// [type] 日历类型 - /// - /// 返回格式化后的年份字符串 - /// 阳历示例:2025 -> "2025年" - /// 阴历示例:2025 -> "二〇二五年" - String formatYear(int year, TCalendarDateType type) { - if (type == TCalendarDateType.solar) { - return '$year年'; - } - return '${_convertToChineseNumber(year)}年'; - } - - /// 格式化月份文本 - /// - /// [month] 月份(1-12) - /// [type] 日历类型 - /// [isLeapMonth] 是否是闰月(仅农历有效) - /// - /// 返回格式化后的月份字符串 - /// 阳历示例:3 -> "3月" - /// 阴历示例:3 -> "三月",闰3月 -> "闰三月" - String formatMonth(int month, TCalendarDateType type, - [bool isLeapMonth = false]) { - if (type == TCalendarDateType.solar) { - return '$month月'; - } - const months = [ - '正月', - '二月', - '三月', - '四月', - '五月', - '六月', - '七月', - '八月', - '九月', - '十月', - '冬月', - '腊月' - ]; - final monthText = months[month - 1]; - return isLeapMonth ? '闰$monthText' : monthText; - } - - /// 格式化日期文本 - /// - /// [day] 日期(1-31) - /// [type] 日历类型 - /// - /// 返回格式化后的日期字符串 - /// 阳历示例:7 -> "7日" - /// 阴历示例:7 -> "初七" - String formatDay(int day, TCalendarDateType type) { - if (type == TCalendarDateType.solar) { - return '$day日'; - } - const days = [ - '初一', - '初二', - '初三', - '初四', - '初五', - '初六', - '初七', - '初八', - '初九', - '初十', - '十一', - '十二', - '十三', - '十四', - '十五', - '十六', - '十七', - '十八', - '十九', - '二十', - '廿一', - '廿二', - '廿三', - '廿四', - '廿五', - '廿六', - '廿七', - '廿八', - '廿九', - '三十' - ]; - return days[day - 1]; - } - - /// 将数字转换为中文数字 - String _convertToChineseNumber(int number) { - const digits = ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九']; - return number - .toString() - .split('') - .map((d) => digits[int.parse(d)]) - .join(); - } + /// 副标题文案;返回 null 或空字符串时不显示副标题行。 + String? getSubtitle(DateTime date) => null; } diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_style.dart b/tdesign-component/lib/src/components/calendar/t_calendar_style.dart index a1f23925f..5f792e146 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar_style.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar_style.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../../../tdesign_flutter.dart'; +import 't_calendar_cell.dart'; /// 日历组件样式 class TCalendarStyle { @@ -10,12 +11,11 @@ class TCalendarStyle { this.titleCloseColor, this.weekdayStyle, this.monthTitleStyle, - this.cellStyle, - this.todayStyle, + this.dayStyle, + this.todayDayStyle, this.centreColor, this.cellDecoration, - this.cellPrefixStyle, - this.cellSuffixStyle, + this.subtitleStyle, }); BoxDecoration? decoration; @@ -35,11 +35,11 @@ class TCalendarStyle { /// body区域 年月文字样式 TextStyle? monthTitleStyle; - /// 日期样式 - TextStyle? cellStyle; + /// 日期主区(默认阳历日数字)样式 + TextStyle? dayStyle; - /// 当天日期样式 - TextStyle? todayStyle; + /// 今天日期主区样式 + TextStyle? todayDayStyle; /// 日期decoration BoxDecoration? cellDecoration; @@ -47,11 +47,8 @@ class TCalendarStyle { /// 日期范围内背景样式 Color? centreColor; - /// 日期前面的字符串的样式 - TextStyle? cellPrefixStyle; - - /// 日期后面的字符串的样式 - TextStyle? cellSuffixStyle; + /// 副标题样式(仅 [TCalendarDataSource.getSubtitle] 字符串路径使用) + TextStyle? subtitleStyle; /// 日期垂直间距,水平间距为[verticalGap] / 2 double? verticalGap; @@ -87,15 +84,15 @@ class TCalendarStyle { bodyPadding = TTheme.of(context).spacer16; } - /// 日期样式 - TCalendarStyle.cellStyle(BuildContext context, DateSelectType? type) { + /// 按选中态生成单元格样式 + TCalendarStyle.forSelectType(BuildContext context, DateSelectType? type) { final radius6 = TTheme.of(context).radiusDefault; final defStyle = TextStyle( fontSize: TTheme.of(context).fontTitleMedium?.size, height: TTheme.of(context).fontTitleMedium?.height, fontWeight: TTheme.of(context).fontTitleMedium?.fontWeight, ); - final prefixStyle = TextStyle( + final subtitleBase = TextStyle( fontSize: TTheme.of(context).fontBodyExtraSmall?.size, height: TTheme.of(context).fontBodyExtraSmall?.height, fontWeight: FontWeight.w400, @@ -103,64 +100,54 @@ class TCalendarStyle { centreColor = TTheme.of(context).brandLightColor; switch (type) { case DateSelectType.empty: - cellStyle = + dayStyle = defStyle.copyWith(color: TTheme.of(context).textColorPrimary); - todayStyle = defStyle.copyWith(color: TTheme.of(context).brandNormalColor); - cellPrefixStyle = - prefixStyle.copyWith(color: TTheme.of(context).errorNormalColor); - cellSuffixStyle = prefixStyle.copyWith( + todayDayStyle = + defStyle.copyWith(color: TTheme.of(context).brandNormalColor); + subtitleStyle = subtitleBase.copyWith( color: TTheme.of(context).textColorPlaceholder); cellDecoration = null; break; case DateSelectType.disabled: - cellStyle = + dayStyle = defStyle.copyWith(color: TTheme.of(context).textDisabledColor); - todayStyle = defStyle.copyWith(color: TTheme.of(context).brandDisabledColor); - cellPrefixStyle = - prefixStyle.copyWith(color: TTheme.of(context).errorDisabledColor); - cellSuffixStyle = - prefixStyle.copyWith(color: TTheme.of(context).textDisabledColor); + todayDayStyle = + defStyle.copyWith(color: TTheme.of(context).brandDisabledColor); + subtitleStyle = + subtitleBase.copyWith(color: TTheme.of(context).textDisabledColor); cellDecoration = null; break; case DateSelectType.selected: - cellStyle = defStyle.copyWith(color: TTheme.of(context).textColorAnti); - cellPrefixStyle = - prefixStyle.copyWith(color: TTheme.of(context).textColorAnti); - cellSuffixStyle = - prefixStyle.copyWith(color: TTheme.of(context).textColorAnti); + dayStyle = defStyle.copyWith(color: TTheme.of(context).textColorAnti); + subtitleStyle = + subtitleBase.copyWith(color: TTheme.of(context).textColorAnti); cellDecoration = BoxDecoration( borderRadius: BorderRadius.circular(radius6), color: TTheme.of(context).brandNormalColor, ); break; case DateSelectType.centre: - cellStyle = + dayStyle = defStyle.copyWith(color: TTheme.of(context).textColorPrimary); - cellPrefixStyle = - prefixStyle.copyWith(color: TTheme.of(context).errorNormalColor); - cellSuffixStyle = prefixStyle.copyWith( + subtitleStyle = subtitleBase.copyWith( color: TTheme.of(context).textColorPlaceholder); cellDecoration = BoxDecoration( color: centreColor, ); break; case DateSelectType.start: - cellStyle = defStyle.copyWith(color: TTheme.of(context).textColorAnti); - cellPrefixStyle = - prefixStyle.copyWith(color: TTheme.of(context).textColorAnti); - cellSuffixStyle = - prefixStyle.copyWith(color: TTheme.of(context).textColorAnti); + dayStyle = defStyle.copyWith(color: TTheme.of(context).textColorAnti); + subtitleStyle = + subtitleBase.copyWith(color: TTheme.of(context).textColorAnti); cellDecoration = BoxDecoration( color: TTheme.of(context).brandNormalColor, borderRadius: BorderRadius.horizontal(left: Radius.circular(radius6)), ); break; case DateSelectType.end: - cellStyle = defStyle.copyWith(color: TTheme.of(context).textColorAnti); - cellPrefixStyle = - prefixStyle.copyWith(color: TTheme.of(context).textColorAnti); - cellSuffixStyle = - prefixStyle.copyWith(color: TTheme.of(context).textColorAnti); + dayStyle = defStyle.copyWith(color: TTheme.of(context).textColorAnti); + subtitleStyle = + subtitleBase.copyWith(color: TTheme.of(context).textColorAnti); cellDecoration = BoxDecoration( color: TTheme.of(context).brandNormalColor, borderRadius: diff --git a/tdesign-component/lib/src/components/calendar/t_lunar_date.dart b/tdesign-component/lib/src/components/calendar/t_lunar_date.dart index e0904e30b..12fa3a5a8 100644 --- a/tdesign-component/lib/src/components/calendar/t_lunar_date.dart +++ b/tdesign-component/lib/src/components/calendar/t_lunar_date.dart @@ -59,12 +59,3 @@ class TLunarInfo { day.hashCode ^ isLeapMonth.hashCode; } - -/// 日历类型枚举 -enum TCalendarDateType { - /// 阳历(公历) - solar, - - /// 阴历(农历) - lunar, -} diff --git a/tdesign-component/test/t_calendar_lunar_test.dart b/tdesign-component/test/t_calendar_lunar_test.dart index 0ec34aecc..58b9fae25 100644 --- a/tdesign-component/test/t_calendar_lunar_test.dart +++ b/tdesign-component/test/t_calendar_lunar_test.dart @@ -17,210 +17,43 @@ void main() { expect(lunarInfo.month, 3); expect(lunarInfo.day, 7); expect(lunarInfo.isLeapMonth, false); - expect(lunarInfo.yearText, '二〇二五'); - expect(lunarInfo.monthText, '三月'); - expect(lunarInfo.dayText, '初七'); expect(lunarInfo.fullText, '二〇二五年 三月初七'); }); - - test('should handle leap month correctly', () { - const lunarInfo = TLunarInfo( - year: 2025, - month: 3, - day: 7, - isLeapMonth: true, - yearText: '二〇二五', - monthText: '闰三月', - dayText: '初七', - ); - - expect(lunarInfo.isLeapMonth, true); - expect(lunarInfo.monthText, '闰三月'); - expect(lunarInfo.fullText, '二〇二五年 闰三月初七'); - }); - - test('should compare lunar info correctly', () { - const info1 = TLunarInfo( - year: 2025, - month: 3, - day: 7, - yearText: '二〇二五', - monthText: '三月', - dayText: '初七', - ); - - const info2 = TLunarInfo( - year: 2025, - month: 3, - day: 7, - yearText: '二〇二五', - monthText: '三月', - dayText: '初七', - ); - - const info3 = TLunarInfo( - year: 2025, - month: 3, - day: 8, - yearText: '二〇二五', - monthText: '三月', - dayText: '初八', - ); - - expect(info1, equals(info2)); - expect(info1, isNot(equals(info3))); - expect(info1.hashCode, equals(info2.hashCode)); - }); }); group('TCalendarDataSource', () { - test('should format year correctly', () { + test('getSubtitle 默认返回 null', () { final dataSource = _MockDataSource(); - - // 阳历 - expect( - dataSource.formatYear(2025, TCalendarDateType.solar), - '2025年', - ); - - // 农历 - expect( - dataSource.formatYear(2025, TCalendarDateType.lunar), - '二〇二五年', - ); + expect(dataSource.getSubtitle(DateTime(2025, 6, 15)), isNull); }); - test('should format month correctly', () { - final dataSource = _MockDataSource(); - - // 阳历 + test('getSubtitle 可返回自定义副标题', () { + final dataSource = _SubtitleDataSource(); expect( - dataSource.formatMonth(3, TCalendarDateType.solar), - '3月', - ); - - // 农历 - expect( - dataSource.formatMonth(1, TCalendarDateType.lunar), - '正月', - ); - expect( - dataSource.formatMonth(3, TCalendarDateType.lunar), - '三月', - ); - expect( - dataSource.formatMonth(11, TCalendarDateType.lunar), - '冬月', - ); - expect( - dataSource.formatMonth(12, TCalendarDateType.lunar), - '腊月', - ); - - // 闰月 - expect( - dataSource.formatMonth(3, TCalendarDateType.lunar, true), - '闰三月', - ); - }); - - test('should format day correctly', () { - final dataSource = _MockDataSource(); - - // 阳历 - expect( - dataSource.formatDay(7, TCalendarDateType.solar), - '7日', - ); - - // 农历 - expect( - dataSource.formatDay(1, TCalendarDateType.lunar), - '初一', - ); - expect( - dataSource.formatDay(7, TCalendarDateType.lunar), + dataSource.getSubtitle(DateTime(2025, 6, 15)), '初七', ); - expect( - dataSource.formatDay(15, TCalendarDateType.lunar), - '十五', - ); - expect( - dataSource.formatDay(21, TCalendarDateType.lunar), - '廿一', - ); - expect( - dataSource.formatDay(30, TCalendarDateType.lunar), - '三十', - ); }); }); - group('TDate with LunarInfo', () { - test('should create TDate with lunar info', () { - const lunarInfo = TLunarInfo( - year: 2025, - month: 3, - day: 7, - yearText: '二〇二五', - monthText: '三月', - dayText: '初七', - ); - - final tdate = TDate( - date: DateTime(2025, 4, 5), - typeNotifier: DateSelectTypeNotifier(DateSelectType.empty), - isLastDayOfMonth: false, - lunarInfo: lunarInfo, - ); - - expect(tdate.date, DateTime(2025, 4, 5)); - expect(tdate.lunarInfo, lunarInfo); - expect(tdate.lunarInfo?.dayText, '初七'); - }); - - test('should create TDate without lunar info', () { - final tdate = TDate( - date: DateTime(2025, 4, 5), + group('TCalendarCellModel', () { + test('selectType 随 typeNotifier 更新', () { + final cell = TCalendarCellModel( + date: DateTime(2025, 6, 15), typeNotifier: DateSelectTypeNotifier(DateSelectType.empty), isLastDayOfMonth: false, ); - expect(tdate.date, DateTime(2025, 4, 5)); - expect(tdate.lunarInfo, isNull); + expect(cell.selectType, DateSelectType.empty); + cell.typeNotifier.setType(DateSelectType.selected); + expect(cell.selectType, DateSelectType.selected); }); }); } -/// Mock 数据源用于测试 -class _MockDataSource extends TCalendarDataSource { - @override - TLunarInfo? getLunarInfo(DateTime solarDate) { - // 简单的 mock 实现 - return const TLunarInfo( - year: 2025, - month: 3, - day: 7, - yearText: '二〇二五', - monthText: '三月', - dayText: '初七', - ); - } +class _MockDataSource extends TCalendarDataSource {} +class _SubtitleDataSource extends TCalendarDataSource { @override - String formatDate( - DateTime date, - TCalendarDateType type, [ - TLunarInfo? lunarInfo, - ]) { - if (type == TCalendarDateType.solar) { - return '${date.year}年${date.month}月${date.day}日'; - } else { - if (lunarInfo != null) { - return '${lunarInfo.yearText} ${lunarInfo.monthText}${lunarInfo.dayText}'; - } - return '${date.year}年${date.month}月${date.day}日'; - } - } + String? getSubtitle(DateTime date) => '初七'; } From 5f4b3ac5de63ad79e66c834efa79eeb767a10c06 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 07:38:38 +0000 Subject: [PATCH 29/35] [autofix.ci] apply automated fixes --- .../example/assets/api/calendar_api.md | 31 +++-- tdesign-site/src/calendar/README.md | 107 ++++++++++-------- 2 files changed, 80 insertions(+), 58 deletions(-) diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md index 729458123..d4c3e4bd9 100644 --- a/tdesign-component/example/assets/api/calendar_api.md +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -6,10 +6,9 @@ | --- | --- | --- | --- | | anchorDate | DateTime? | - | 锚点日期,弹出时自动滚动到该日期所在月份。 | | animateTo | bool | false | 滚动到选中日期/锚点日期所在月份时是否使用动画,默认 false | +| cellBuilder | TCalendarCellBuilder? | - | 整格自定义;设置后不再使用默认主区/副标题布局。 | | cellHeight | double? | - | 日期单元格高度,默认 60。如需更大行高可传入自定义值(如 80) | -| cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | -| dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能。 | -| displayMode | TCalendarDisplayMode | TCalendarDisplayMode.solar | 日历显示模式,控制日期单元格的主/副文本内容: | +| dataSource | TCalendarDataSource? | - | 可选数据源,提供副标题字符串(无 [subtitleBuilder] 时生效)。 | | firstDayOfWeek | int | 0 | 第一天从星期几开始,0 = 周日,1 = 周一,…,6 = 周六。默认 0(周日)。 | | height | double? | - | 高度,不传时内嵌模式自动按 5 行日期计算 | | initialValue | List? | - | 初始选中日期列表,不传则默认今天。 | @@ -18,11 +17,12 @@ | minDate | DateTime? | - | 最小可选的日期,不传则默认 1970-01-01 | | monthTitleBuilder | Widget Function(BuildContext context, DateTime monthDate)? | - | 月标题构建器 | | monthTitleHeight | double | 22 | 每月标题行高度(如 '2025年6月' 所在行),默认 22 | -| onCellClick | void Function(DateTime value, DateSelectType selectType, TDate tdate)? | - | 点击日期时触发 | +| onCellClick | void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? | - | 点击日期时触发 | | onChange | void Function(List value)? | - | 选中值变化时触发 | | onMonthChange | ValueChanged? | - | 月份变化时触发 | | safeAreaInset | bool | true | 是否适配底部安全区域(如 iPhone Home Indicator),默认 true | | style | TCalendarStyle? | - | 自定义样式 | +| subtitleBuilder | TCalendarSubtitleBuilder? | - | 副标题完全自定义;未设置时可使用 [dataSource.getSubtitle]。 | | titleWidget | Widget? | - | 标题组件,可传入 Text 或自定义 Widget | | type | CalendarType | CalendarType.single | 日历的选择模式,决定点击日期后的选中行为: | @@ -31,7 +31,7 @@ | 名称 | 返回类型 | 参数 | 说明 | | --- | --- | --- | --- | -| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget? confirmBtn, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TDate tdate)? onCellClick, bool autoClose, bool draggable, Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? cellWidget, TCalendarDisplayMode displayMode, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | +| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget? confirmBtn, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick, bool autoClose, bool draggable, TCalendarCellBuilder? cellBuilder, TCalendarSubtitleBuilder? subtitleBuilder, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | ``` ``` @@ -70,16 +70,15 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | cellDecoration | BoxDecoration? | - | 日期decoration | -| cellPrefixStyle | TextStyle? | - | 日期前面的字符串的样式 | -| cellStyle | TextStyle? | - | 日期样式 | -| cellSuffixStyle | TextStyle? | - | 日期后面的字符串的样式 | | centreColor | Color? | - | 日期范围内背景样式 | +| dayStyle | TextStyle? | - | 日期主区(默认阳历日数字)样式 | | decoration | | - | | | monthTitleStyle | TextStyle? | - | body区域 年月文字样式 | +| subtitleStyle | TextStyle? | - | 副标题样式(仅 [TCalendarDataSource.getSubtitle] 字符串路径使用) | | titleCloseColor | Color? | - | header区域 关闭图标的颜色 | | titleMaxLine | int? | - | header区域 [TCalendar.titleWidget]的行数 | | titleStyle | TextStyle? | - | header区域 [TCalendar.titleWidget]的样式 | -| todayStyle | TextStyle? | - | 当天日期样式 | +| todayDayStyle | TextStyle? | - | 今天日期主区样式 | | weekdayStyle | TextStyle? | - | header区域 周 文字样式 | @@ -87,7 +86,7 @@ | 名称 | 说明 | | --- | --- | -| TCalendarStyle.cellStyle | 日期样式 | +| TCalendarStyle.forSelectType | 按选中态生成单元格样式 | | TCalendarStyle.generateStyle | 生成默认样式 | ``` @@ -97,6 +96,18 @@ ``` ``` +### TCalendarCellModel +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| date | | - | | +| isLastDayOfMonth | | - | | +| typeNotifier | | - | | + +``` +``` + ### TLunarInfo #### 默认构造方法 diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md index 9c7971842..455a2f47f 100644 --- a/tdesign-site/src/calendar/README.md +++ b/tdesign-site/src/calendar/README.md @@ -68,7 +68,7 @@ Widget _buildStyle(BuildContext context) { final cellDate = cellValue[0]; return TCellGroup( cells: [ - // 1. 自定义文案(cellWidget,仅 showPopup 弹窗模式) + // 1. 自定义文案(cellBuilder,仅 showPopup 弹窗模式) TCell( title: '自定义文案', arrow: true, @@ -81,16 +81,16 @@ Widget _buildStyle(BuildContext context) { minDate: DateTime(2022, 1, 1), maxDate: DateTime(2022, 2, 15), onConfirm: (value) => customTextSelected.value = value, - cellWidget: (context, tdate, selectType) { - final isSpecial = tdate.date.month == 2 && - map.keys.contains(tdate.date.day); - final suffix = isSpecial ? '¥100' : '¥60'; - final prefix = isSpecial ? map[tdate.date.day] : null; + cellBuilder: (context, cell) { + final isSpecial = cell.date.month == 2 && + map.keys.contains(cell.date.day); + final sub = isSpecial ? '¥100' : '¥60'; + final top = isSpecial ? map[cell.date.day] : null; return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (prefix != null) - Text(prefix, + if (top != null) + Text(top, style: TextStyle( fontSize: 9, color: isSpecial @@ -98,19 +98,19 @@ Widget _buildStyle(BuildContext context) { : null, )), Text( - tdate.date.day.toString(), + cell.date.day.toString(), style: TextStyle( - color: selectType == DateSelectType.selected + color: cell.selectType == DateSelectType.selected ? TTheme.of(context).fontWhColor1 : isSpecial ? TTheme.of(context).errorColor6 : null, ), ), - Text(suffix, + Text(sub, style: TextStyle( fontSize: 9, - color: selectType == DateSelectType.selected + color: cell.selectType == DateSelectType.selected ? TTheme.of(context).fontWhColor1 : isSpecial ? TTheme.of(context).errorColor6 @@ -150,7 +150,7 @@ Widget _buildStyle(BuildContext context) { }, ), - // 3. 自定义日期单元格(cellWidget 回调) + // 3. 自定义日期单元格(cellBuilder 回调) TCell( title: '自定义日期单元格', arrow: true, @@ -162,12 +162,12 @@ Widget _buildStyle(BuildContext context) { initialValue: cellValue, cellHeight: 80, onConfirm: (value) => customCellSelected.value = value, - cellWidget: (context, tdate, selectType) { + cellBuilder: (context, cell) { final today = DateTime.now(); - final isToday = tdate.date == + final isToday = cell.date == DateTime(today.year, today.month, today.day); - if (isToday && selectType != DateSelectType.selected) { + if (isToday && cell.selectType != DateSelectType.selected) { return _CustomCellContainer( color: TTheme.of(context).brandColor4, child: const Text('今天', @@ -177,13 +177,13 @@ Widget _buildStyle(BuildContext context) { color: Colors.white)), ); } - if (selectType == DateSelectType.selected) { + if (cell.selectType == DateSelectType.selected) { return _CustomCellContainer( color: TTheme.of(context).successColor8, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('${tdate.date.day}', + Text('${cell.date.day}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -198,7 +198,7 @@ Widget _buildStyle(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('${tdate.date.day}', + Text('${cell.date.day}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold)), const Text('自定义', style: TextStyle(fontSize: 8)), @@ -253,7 +253,7 @@ Widget _buildStyle(BuildContext context) { final cellDate = cellValue[0]; return TCellGroup( cells: [ - // 1. 自定义文案(cellWidget,仅 showPopup 弹窗模式) + // 1. 自定义文案(cellBuilder,仅 showPopup 弹窗模式) TCell( title: '自定义文案', arrow: true, @@ -266,16 +266,16 @@ Widget _buildStyle(BuildContext context) { minDate: DateTime(2022, 1, 1), maxDate: DateTime(2022, 2, 15), onConfirm: (value) => customTextSelected.value = value, - cellWidget: (context, tdate, selectType) { - final isSpecial = tdate.date.month == 2 && - map.keys.contains(tdate.date.day); - final suffix = isSpecial ? '¥100' : '¥60'; - final prefix = isSpecial ? map[tdate.date.day] : null; + cellBuilder: (context, cell) { + final isSpecial = cell.date.month == 2 && + map.keys.contains(cell.date.day); + final sub = isSpecial ? '¥100' : '¥60'; + final top = isSpecial ? map[cell.date.day] : null; return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (prefix != null) - Text(prefix, + if (top != null) + Text(top, style: TextStyle( fontSize: 9, color: isSpecial @@ -283,19 +283,19 @@ Widget _buildStyle(BuildContext context) { : null, )), Text( - tdate.date.day.toString(), + cell.date.day.toString(), style: TextStyle( - color: selectType == DateSelectType.selected + color: cell.selectType == DateSelectType.selected ? TTheme.of(context).fontWhColor1 : isSpecial ? TTheme.of(context).errorColor6 : null, ), ), - Text(suffix, + Text(sub, style: TextStyle( fontSize: 9, - color: selectType == DateSelectType.selected + color: cell.selectType == DateSelectType.selected ? TTheme.of(context).fontWhColor1 : isSpecial ? TTheme.of(context).errorColor6 @@ -335,7 +335,7 @@ Widget _buildStyle(BuildContext context) { }, ), - // 3. 自定义日期单元格(cellWidget 回调) + // 3. 自定义日期单元格(cellBuilder 回调) TCell( title: '自定义日期单元格', arrow: true, @@ -347,12 +347,12 @@ Widget _buildStyle(BuildContext context) { initialValue: cellValue, cellHeight: 80, onConfirm: (value) => customCellSelected.value = value, - cellWidget: (context, tdate, selectType) { + cellBuilder: (context, cell) { final today = DateTime.now(); - final isToday = tdate.date == + final isToday = cell.date == DateTime(today.year, today.month, today.day); - if (isToday && selectType != DateSelectType.selected) { + if (isToday && cell.selectType != DateSelectType.selected) { return _CustomCellContainer( color: TTheme.of(context).brandColor4, child: const Text('今天', @@ -362,13 +362,13 @@ Widget _buildStyle(BuildContext context) { color: Colors.white)), ); } - if (selectType == DateSelectType.selected) { + if (cell.selectType == DateSelectType.selected) { return _CustomCellContainer( color: TTheme.of(context).successColor8, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('${tdate.date.day}', + Text('${cell.date.day}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -383,7 +383,7 @@ Widget _buildStyle(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('${tdate.date.day}', + Text('${cell.date.day}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold)), const Text('自定义', style: TextStyle(fontSize: 8)), @@ -439,10 +439,9 @@ Widget _buildLunar(BuildContext context) { | --- | --- | --- | --- | | anchorDate | DateTime? | - | 锚点日期,弹出时自动滚动到该日期所在月份。 | | animateTo | bool | false | 滚动到选中日期/锚点日期所在月份时是否使用动画,默认 false | +| cellBuilder | TCalendarCellBuilder? | - | 整格自定义;设置后不再使用默认主区/副标题布局。 | | cellHeight | double? | - | 日期单元格高度,默认 60。如需更大行高可传入自定义值(如 80) | -| cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 | -| dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能。 | -| displayMode | TCalendarDisplayMode | TCalendarDisplayMode.solar | 日历显示模式,控制日期单元格的主/副文本内容: | +| dataSource | TCalendarDataSource? | - | 可选数据源,提供副标题字符串(无 [subtitleBuilder] 时生效)。 | | firstDayOfWeek | int | 0 | 第一天从星期几开始,0 = 周日,1 = 周一,…,6 = 周六。默认 0(周日)。 | | height | double? | - | 高度,不传时内嵌模式自动按 5 行日期计算 | | initialValue | List? | - | 初始选中日期列表,不传则默认今天。 | @@ -451,11 +450,12 @@ Widget _buildLunar(BuildContext context) { | minDate | DateTime? | - | 最小可选的日期,不传则默认 1970-01-01 | | monthTitleBuilder | Widget Function(BuildContext context, DateTime monthDate)? | - | 月标题构建器 | | monthTitleHeight | double | 22 | 每月标题行高度(如 '2025年6月' 所在行),默认 22 | -| onCellClick | void Function(DateTime value, DateSelectType selectType, TDate tdate)? | - | 点击日期时触发 | +| onCellClick | void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? | - | 点击日期时触发 | | onChange | void Function(List value)? | - | 选中值变化时触发 | | onMonthChange | ValueChanged? | - | 月份变化时触发 | | safeAreaInset | bool | true | 是否适配底部安全区域(如 iPhone Home Indicator),默认 true | | style | TCalendarStyle? | - | 自定义样式 | +| subtitleBuilder | TCalendarSubtitleBuilder? | - | 副标题完全自定义;未设置时可使用 [dataSource.getSubtitle]。 | | titleWidget | Widget? | - | 标题组件,可传入 Text 或自定义 Widget | | type | CalendarType | CalendarType.single | 日历的选择模式,决定点击日期后的选中行为: | @@ -464,7 +464,7 @@ Widget _buildLunar(BuildContext context) { | 名称 | 返回类型 | 参数 | 说明 | | --- | --- | --- | --- | -| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget? confirmBtn, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TDate tdate)? onCellClick, bool autoClose, bool draggable, Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? cellWidget, TCalendarDisplayMode displayMode, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | +| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget? confirmBtn, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick, bool autoClose, bool draggable, TCalendarCellBuilder? cellBuilder, TCalendarSubtitleBuilder? subtitleBuilder, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | ``` ``` @@ -503,16 +503,15 @@ Widget _buildLunar(BuildContext context) { | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | cellDecoration | BoxDecoration? | - | 日期decoration | -| cellPrefixStyle | TextStyle? | - | 日期前面的字符串的样式 | -| cellStyle | TextStyle? | - | 日期样式 | -| cellSuffixStyle | TextStyle? | - | 日期后面的字符串的样式 | | centreColor | Color? | - | 日期范围内背景样式 | +| dayStyle | TextStyle? | - | 日期主区(默认阳历日数字)样式 | | decoration | | - | | | monthTitleStyle | TextStyle? | - | body区域 年月文字样式 | +| subtitleStyle | TextStyle? | - | 副标题样式(仅 [TCalendarDataSource.getSubtitle] 字符串路径使用) | | titleCloseColor | Color? | - | header区域 关闭图标的颜色 | | titleMaxLine | int? | - | header区域 [TCalendar.titleWidget]的行数 | | titleStyle | TextStyle? | - | header区域 [TCalendar.titleWidget]的样式 | -| todayStyle | TextStyle? | - | 当天日期样式 | +| todayDayStyle | TextStyle? | - | 今天日期主区样式 | | weekdayStyle | TextStyle? | - | header区域 周 文字样式 | @@ -520,7 +519,7 @@ Widget _buildLunar(BuildContext context) { | 名称 | 说明 | | --- | --- | -| TCalendarStyle.cellStyle | 日期样式 | +| TCalendarStyle.forSelectType | 按选中态生成单元格样式 | | TCalendarStyle.generateStyle | 生成默认样式 | ``` @@ -530,6 +529,18 @@ Widget _buildLunar(BuildContext context) { ``` ``` +### TCalendarCellModel +#### 默认构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| date | | - | | +| isLastDayOfMonth | | - | | +| typeNotifier | | - | | + +``` +``` + ### TLunarInfo #### 默认构造方法 From a48a1861f7257e22ba3b8db4aabdd59abb11778b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Wed, 20 May 2026 16:03:44 +0800 Subject: [PATCH 30/35] =?UTF-8?q?feat(calendar):=20=E5=BC=95=E5=85=A5=20Lu?= =?UTF-8?q?narInfo=20=E7=B1=BB=E4=BB=A5=E6=94=AF=E6=8C=81=E5=86=9C?= =?UTF-8?q?=E5=8E=86=E4=BF=A1=E6=81=AF=EF=BC=8C=E6=9B=B4=E6=96=B0=E6=97=A5?= =?UTF-8?q?=E5=8E=86=E7=BB=84=E4=BB=B6=E9=80=BB=E8=BE=91=E5=92=8C=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tdesign-component/demo_tool/all_build.sh | 2 +- .../lib/lunar_data_source_example.dart | 10 +- .../lib/lunar_info.dart} | 25 ++--- .../example/lib/page/t_calendar_page.dart | 3 + .../example/test/lunar_info_test.dart | 21 ++++ .../src/components/calendar/t_calendar.dart | 105 ++++++++---------- .../components/calendar/t_calendar_body.dart | 36 ++++-- .../test/t_calendar_lunar_test.dart | 19 ---- tdesign-component/test/t_calendar_test.dart | 88 ++++++++++++++- 9 files changed, 192 insertions(+), 117 deletions(-) rename tdesign-component/{lib/src/components/calendar/t_lunar_date.dart => example/lib/lunar_info.dart} (68%) create mode 100644 tdesign-component/example/test/lunar_info_test.dart diff --git a/tdesign-component/demo_tool/all_build.sh b/tdesign-component/demo_tool/all_build.sh index a2fbdff45..222124d19 100644 --- a/tdesign-component/demo_tool/all_build.sh +++ b/tdesign-component/demo_tool/all_build.sh @@ -40,7 +40,7 @@ dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/compo # 输入 # calendar -dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/calendar" --name TCalendar,TCalendarInherited,TCalendarStyle,TCalendarDataSource,TCalendarCellModel,TLunarInfo --folder-name calendar --output "$PARENT_DIR/example/assets/api/" --only-api +dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/calendar" --name TCalendar,TCalendarInherited,TCalendarStyle,TCalendarDataSource,TCalendarCellModel --folder-name calendar --output "$PARENT_DIR/example/assets/api/" --only-api # cascader dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/cascader" --name TMultiCascader --folder-name cascader --output "$PARENT_DIR/example/assets/api/" --only-api diff --git a/tdesign-component/example/lib/lunar_data_source_example.dart b/tdesign-component/example/lib/lunar_data_source_example.dart index 1201af4bd..063cd182b 100644 --- a/tdesign-component/example/lib/lunar_data_source_example.dart +++ b/tdesign-component/example/lib/lunar_data_source_example.dart @@ -1,5 +1,7 @@ -import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:lunar/lunar.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +import 'lunar_info.dart'; /// 基于 lunar 库的农历示例:副标题由 [getSubtitle] 或 [subtitleBuilder] 接入。 class LunarDataSourceExample extends TCalendarDataSource { @@ -67,12 +69,12 @@ class LunarDataSourceExample extends TCalendarDataSource { return days[day - 1]; } - TLunarInfo? getLunarInfo(DateTime solarDate) { + LunarInfo? getLunarInfo(DateTime solarDate) { try { final solar = Solar.fromDate(solarDate); final lunar = solar.getLunar(); - return TLunarInfo( + return LunarInfo( year: lunar.getYear(), month: lunar.getMonth().abs(), day: lunar.getDay(), @@ -141,7 +143,7 @@ class LunarDataSourceExample extends TCalendarDataSource { return solarTerms[key]; } - String? festivalOf(DateTime date, [TLunarInfo? lunarInfo]) { + String? festivalOf(DateTime date, [LunarInfo? lunarInfo]) { if (date.month == 1 && date.day == 1) return '元旦'; if (date.month == 2 && date.day == 14) return '情人节'; if (date.month == 5 && date.day == 1) return '劳动节'; diff --git a/tdesign-component/lib/src/components/calendar/t_lunar_date.dart b/tdesign-component/example/lib/lunar_info.dart similarity index 68% rename from tdesign-component/lib/src/components/calendar/t_lunar_date.dart rename to tdesign-component/example/lib/lunar_info.dart index 12fa3a5a8..e44e75919 100644 --- a/tdesign-component/lib/src/components/calendar/t_lunar_date.dart +++ b/tdesign-component/example/lib/lunar_info.dart @@ -1,30 +1,20 @@ import 'package:flutter/foundation.dart'; -/// 农历日期信息模型 +/// 农历日期信息(仅示例用,业务请自行定义模型)。 +/// +/// 日历组件只要求 [TCalendarDataSource.getSubtitle] 返回字符串; +/// 本类演示如何从农历库组装副标题,非 `tdesign_flutter` 公共 API。 @immutable -class TLunarInfo { - /// 农历年份(数字) +class LunarInfo { final int year; - - /// 农历月份(数字,1-12) final int month; - - /// 农历日期(数字,1-30) final int day; - - /// 是否是闰月 final bool isLeapMonth; - - /// 年份文本(如:二〇二五) final String yearText; - - /// 月份文本(如:三月、闰三月) final String monthText; - - /// 日期文本(如:初七) final String dayText; - const TLunarInfo({ + const LunarInfo({ required this.year, required this.month, required this.day, @@ -34,7 +24,6 @@ class TLunarInfo { required this.dayText, }); - /// 获取完整的农历日期文本 String get fullText => '$yearText年 $monthText$dayText'; @override @@ -45,7 +34,7 @@ class TLunarInfo { if (identical(this, other)) { return true; } - return other is TLunarInfo && + return other is LunarInfo && other.year == year && other.month == month && other.day == day && diff --git a/tdesign-component/example/lib/page/t_calendar_page.dart b/tdesign-component/example/lib/page/t_calendar_page.dart index 1c6c9e7de..19d430e2e 100644 --- a/tdesign-component/example/lib/page/t_calendar_page.dart +++ b/tdesign-component/example/lib/page/t_calendar_page.dart @@ -1078,6 +1078,7 @@ class _LunarCalendarDemoState extends State<_LunarCalendarDemo> { static final DateTime _maxDate = DateTime(2030, 12, 31); DateTime? _anchorDate; + int _anchorRevision = 0; List _selected = [DateTime.now()]; @override @@ -1093,6 +1094,7 @@ class _LunarCalendarDemoState extends State<_LunarCalendarDemo> { onNavigate: (anchor) { setState(() { _anchorDate = anchor; + _anchorRevision++; }); }, ), @@ -1103,6 +1105,7 @@ class _LunarCalendarDemoState extends State<_LunarCalendarDemo> { maxDate: _maxDate, initialValue: _selected, anchorDate: _anchorDate, + anchorRevision: _anchorRevision, animateTo: true, dataSource: _dataSource, onMonthChange: (month) { diff --git a/tdesign-component/example/test/lunar_info_test.dart b/tdesign-component/example/test/lunar_info_test.dart new file mode 100644 index 000000000..8df3f3980 --- /dev/null +++ b/tdesign-component/example/test/lunar_info_test.dart @@ -0,0 +1,21 @@ +import 'package:flutter_test/flutter_test.dart'; + +import '../lib/lunar_info.dart'; + +void main() { + group('LunarInfo (example)', () { + test('creates and formats fullText', () { + const lunarInfo = LunarInfo( + year: 2025, + month: 3, + day: 7, + yearText: '二〇二五', + monthText: '三月', + dayText: '初七', + ); + + expect(lunarInfo.year, 2025); + expect(lunarInfo.fullText, '二〇二五年 三月初七'); + }); + }); +} diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart index 901df442d..ad280a989 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart @@ -17,7 +17,6 @@ export 't_calendar_cell.dart' TCalendarCellBuilder; export 't_calendar_data_source.dart'; export 't_calendar_style.dart'; -export 't_lunar_date.dart'; // --------------------------------------------------------------------------- // TCalendarInherited — 日历弹窗状态托管,供 TCalendar 内部读取 @@ -36,7 +35,6 @@ class TCalendarInherited extends InheritedWidget { this.popupControls = true, this.popupConfirmBtn, this.onConfirm, - this.confirmBtn, this.confirmBtnBuilder, this.popupBottomBuilder, this.popupBottomExpanded, @@ -75,10 +73,7 @@ class TCalendarInherited extends InheritedWidget { final VoidCallback? onConfirm; - /// 自定义确认按钮(静态 Widget,需自行处理点击;推荐 [confirmBtnBuilder])。 - final Widget? confirmBtn; - - /// 自定义确认按钮构建器,[onConfirm] 与默认确认按钮行为一致(回传选中值并关闭弹窗)。 + /// 自定义确认按钮;[onConfirm] 与默认确认按钮一致(回传选中值并关闭弹窗)。 final Widget Function(VoidCallback onConfirm)? confirmBtnBuilder; /// 弹窗底部自定义区域构建器(仅弹窗模式,由 [TCalendar.showPopup] 或手动 @@ -89,8 +84,22 @@ class TCalendarInherited extends InheritedWidget { /// 弹窗底部区域是否展开(响应式),需配合 [popupBottomBuilder]。 final ValueListenable? popupBottomExpanded; + /// 仅当 Inherited 上的**静态配置**变化时通知依赖方重建。 + /// + /// [selected] 为 [ValueNotifier],变更走 [selectedListenable],不依赖本方法。 + /// 若返回 `false`,在运行期替换 [popupBottomBuilder] 等回调时,子树不会自动重建, + /// 弹窗场景一般在 push 时一次性注入,内嵌高级用法请整体替换 Inherited。 @override - bool updateShouldNotify(covariant TCalendarInherited oldWidget) => false; + bool updateShouldNotify(covariant TCalendarInherited oldWidget) { + return oldWidget.usePopup != usePopup || + oldWidget.popupControls != popupControls || + oldWidget.popupConfirmBtn != popupConfirmBtn || + oldWidget.onClose != onClose || + oldWidget.onConfirm != onConfirm || + oldWidget.confirmBtnBuilder != confirmBtnBuilder || + oldWidget.popupBottomBuilder != popupBottomBuilder || + oldWidget.popupBottomExpanded != popupBottomExpanded; + } static TCalendarInherited? of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType(); @@ -132,6 +141,7 @@ class TCalendar extends StatefulWidget { this.subtitleBuilder, this.onMonthChange, this.anchorDate, + this.anchorRevision = 0, this.dataSource, }) : super(key: key); @@ -155,6 +165,10 @@ class TCalendar extends StatefulWidget { /// 初始选中日期列表,不传则默认今天。 /// + /// **非受控语义**:仅用于首次挂载;用户点选后以 [onChange] 为准,由调用方自行 + /// `setState` 保存。若父组件在运行期修改本参数,会同步选中态并刷新格子(与 range + /// 行为一致)。 + /// /// 列表长度与 [type] 对应: /// - [CalendarType.single]:1 个元素(选中日期) /// - [CalendarType.multiple]:N 个元素(所有选中日期) @@ -204,9 +218,14 @@ class TCalendar extends StatefulWidget { /// 副标题完全自定义;未设置时可使用 [dataSource.getSubtitle]。 final TCalendarSubtitleBuilder? subtitleBuilder; - /// 锚点日期,弹出时自动滚动到该日期所在月份。 + /// 锚点日期,打开时滚动到该日期所在月份。 final DateTime? anchorDate; + /// 锚点滚动触发序号,默认 `0`。 + /// + /// 与 [anchorDate] 配合:序号递增可重复滚到同一月份;仅改月份时也可只更新 [anchorDate]。 + final int anchorRevision; + /// 可选数据源,提供副标题字符串(无 [subtitleBuilder] 时生效)。 final TCalendarDataSource? dataSource; @@ -242,6 +261,8 @@ class TCalendar extends StatefulWidget { /// 弹出日历选择器,返回选中的日期列表。 /// /// 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 + /// 弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`, + /// 或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。 /// /// ```dart /// final result = await TCalendar.showPopup( @@ -276,6 +297,9 @@ class TCalendar extends StatefulWidget { /// 锚点日期,弹出时自动滚动到该日期所在月份 DateTime? anchorDate, + /// 锚点滚动触发序号,见 [TCalendar.anchorRevision] + int anchorRevision = 0, + /// 弹窗面板高度(不传时自动计算) double? popupHeight, @@ -295,13 +319,7 @@ class TCalendar extends StatefulWidget { /// 弹窗底部区域是否展开(响应式),需配合 [popupBottomBuilder]。 ValueListenable? popupBottomExpanded, - /// 自定义确认按钮(静态 Widget)。 - /// - /// 若未设置 [onTap],[showPopup] 会自动为其叠加点击层以触发确认; - /// 需要按钮按压态时请改用 [confirmBtnBuilder]。 - Widget? confirmBtn, - - /// 自定义确认按钮构建器,[onConfirm] 回调与默认确认按钮一致。 + /// 自定义确认按钮,[onConfirm] 与默认确认按钮一致。 Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, /// 点击确认按钮时触发 @@ -362,20 +380,12 @@ class TCalendar extends StatefulWidget { panelTitle = _extractTextFromWidget(titleWidget); } - final effectiveConfirmBtnBuilder = confirmBtnBuilder ?? - (confirmBtn != null - ? (onConfirm) => _CalendarConfirmTapProxy( - onConfirm: onConfirm, - child: confirmBtn, - ) - : null); - return TCalendarInherited( selected: selected, usePopup: true, popupControls: false, popupConfirmBtn: true, - confirmBtnBuilder: effectiveConfirmBtnBuilder, + confirmBtnBuilder: confirmBtnBuilder, popupBottomBuilder: popupBottomBuilder, popupBottomExpanded: popupBottomExpanded, onClose: () { @@ -406,6 +416,7 @@ class TCalendar extends StatefulWidget { minDate: minDate, maxDate: maxDate, anchorDate: anchorDate, + anchorRevision: anchorRevision, firstDayOfWeek: firstDayOfWeek, cellHeight: cellHeight, style: style, @@ -452,12 +463,12 @@ class _TCalendarState extends State { List? _cachedValueDates; - /// single 模式下当前选中的 TDate 引用(来自 body 缓存的当前实例)。 + /// single 模式下当前选中的单元格引用(来自 body 缓存的当前实例)。 /// /// cell 不再反查 `_data` 找上一个 selected:state 维护这条权威引用,点击 /// 时直接 setType(empty) 即可。引用会随 body 缓存重生成(cleanup 后再滚回 - /// 该月)被 [_handleTDateGenerated] 覆盖为新实例,不会出现"指向已 detach - /// 的 TDate"导致视觉残留。 + /// 该月)被 [_handleCellGenerated] 覆盖为新实例,不会出现"指向已 detach + /// 的 cell"导致视觉残留。 TCalendarCellModel? _selectedSingleRef; /// multiple 模式下当前所有选中的单元格引用,按日期键。 @@ -621,9 +632,6 @@ class _TCalendarState extends State { if (inherited?.confirmBtnBuilder != null) { return inherited!.confirmBtnBuilder!(onConfirm ?? () {}); } - if (inherited?.confirmBtn != null) { - return inherited!.confirmBtn!; - } return Padding( padding: widget.safeAreaInset ? EdgeInsets.only(top: TTheme.of(context).spacer16) @@ -669,6 +677,7 @@ class _TCalendarState extends State { firstDayOfWeek: widget.firstDayOfWeek, maxDate: widget.maxDate, anchorDate: widget.anchorDate, + anchorRevision: widget.anchorRevision, minDate: widget.minDate, value: _cachedValueDates, bodyPadding: _style.bodyPadding ?? TTheme.of(context).spacer16, @@ -702,8 +711,8 @@ class _TCalendarState extends State { ); } - /// 月份 TDate 列表新生成时被 body 调用:登记 selected/start/end 引用, - /// 让 state 不依赖 body 内部缓存即可定位"当前选中的那些 TDate 实例"。 + /// 月份单元格列表新生成时被 body 调用:登记 selected 引用, + /// 让 state 不依赖 body 内部缓存即可定位当前选中的 cell 实例。 /// /// single:每月最多一个 selected,遇到即覆盖 _selectedSingleRef。 /// multiple:把当月所有 selected 的引用按 date 写入 map。 @@ -728,13 +737,13 @@ class _TCalendarState extends State { } /// 当 body 整体清空缓存时(minDate/maxDate 变化等),同步清空选中映射, - /// 避免悬挂指向已被替换的 TDate 实例。后续月份重新生成时会再次登记。 + /// 避免悬挂指向已被替换的 cell 实例。后续月份重新生成时会再次登记。 void _handleCacheInvalidated() { _selectedSingleRef = null; _selectedMultipleRefs.clear(); } - /// 三种模式统一入口:cell 仅上抛被点击的 TDate,由本方法做所有决策。 + /// 三种模式统一入口:cell 仅上抛被点击的模型,由本方法做所有决策。 /// /// 行为约定: /// - disabled:仅触发 onCellClick,不改变选中态 @@ -869,7 +878,7 @@ class _TCalendarState extends State { final btnPadding = widget.safeAreaInset ? TTheme.of(context).spacer16 : TTheme.of(context).spacer16 * 2; - // 默认与自定义 confirmBtn 均预留固定高度,避免 popupBottomBuilder 浮层重叠。 + // 默认与自定义确认按钮均预留固定高度,避免 popupBottomBuilder 浮层重叠。 // 若自定义按钮更高,请在 popupHeight 中额外预留空间。 return safeBottom + btnPadding + _confirmBtnHeight; } @@ -903,29 +912,3 @@ class _TCalendarState extends State { bodyPadding * 2; } } - -/// 为未绑定 [onTap] 的静态 [confirmBtn] 叠加透明点击层,触发确认并关闭弹窗。 -class _CalendarConfirmTapProxy extends StatelessWidget { - const _CalendarConfirmTapProxy({ - required this.onConfirm, - required this.child, - }); - - final VoidCallback onConfirm; - final Widget child; - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - child, - Positioned.fill( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onConfirm, - ), - ), - ], - ); - } -} diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart index e5f7c88f1..904946c80 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart @@ -23,6 +23,7 @@ class TCalendarBody extends StatefulWidget { required this.animateTo, this.onMonthChange, this.anchorDate, + this.anchorRevision = 0, this.onCellGenerated, this.onCacheInvalidated, }) : super(key: key); @@ -51,6 +52,12 @@ class TCalendarBody extends StatefulWidget { final double cellHeight; final bool animateTo; final ValueChanged? onMonthChange; + + /// 锚点滚动触发序号。与 [anchorDate] 配合:序号变化或锚点目标月份变化时滚动。 + /// + /// 重复导航到同一月份时递增即可,无需每次 `new DateTime`。 + final int anchorRevision; + /// 在每个月份的单元格列表新生成时回调,便于上层登记选中引用。 final void Function(DateTime monthDate, List cells)? onCellGenerated; @@ -106,23 +113,32 @@ class _TCalendarBodyState extends State { widget.onCacheInvalidated?.call(); _lastNotifiedMonthKey = null; _initMonths(); - } else if (widget.type == CalendarType.range && - !_listEqualsDate(oldWidget.value, widget.value)) { - // range 模式下,state 通过更新 value 来传达选中区间变化。 - // 必须清空 _data,让所有月份重新基于新 value 推导 cell 类型, - // 避免跨月份 cell 的 typeNotifier 残留旧 start/end/centre 状态。 + } else if (!_listEqualsDate(oldWidget.value, widget.value)) { + // 选中值由上层更新(initialValue / _cachedValueDates)时清空月份缓存, + // 让所有 cell 基于新 value 重建 typeNotifier,避免 single/multiple/range 残留旧态。 _data.clear(); widget.onCacheInvalidated?.call(); } - // anchorDate 变化时滚动:使用 identical 引用比较, - // 让上层即使重复传同一年月(例如滑动到该月后再点击该月按钮)也能触发滚动; - // 上层只要每次导航都构造新的 DateTime 实例即可。 - final newAnchor = widget.anchorDate; - if (newAnchor != null && !identical(newAnchor, oldWidget.anchorDate)) { + if (_shouldScrollToAnchor(oldWidget)) { _scrollToItem(); } } + bool _shouldScrollToAnchor(TCalendarBody oldWidget) { + final anchor = widget.anchorDate; + if (anchor == null) { + return false; + } + if (widget.anchorRevision != oldWidget.anchorRevision) { + return true; + } + final oldAnchor = oldWidget.anchorDate; + if (oldAnchor == null) { + return true; + } + return anchor.year != oldAnchor.year || anchor.month != oldAnchor.month; + } + static bool _listEqualsDate(List? a, List? b) { if (identical(a, b)) { return true; diff --git a/tdesign-component/test/t_calendar_lunar_test.dart b/tdesign-component/test/t_calendar_lunar_test.dart index 58b9fae25..ee9946335 100644 --- a/tdesign-component/test/t_calendar_lunar_test.dart +++ b/tdesign-component/test/t_calendar_lunar_test.dart @@ -2,25 +2,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; void main() { - group('TLunarInfo', () { - test('should create lunar info correctly', () { - const lunarInfo = TLunarInfo( - year: 2025, - month: 3, - day: 7, - yearText: '二〇二五', - monthText: '三月', - dayText: '初七', - ); - - expect(lunarInfo.year, 2025); - expect(lunarInfo.month, 3); - expect(lunarInfo.day, 7); - expect(lunarInfo.isLeapMonth, false); - expect(lunarInfo.fullText, '二〇二五年 三月初七'); - }); - }); - group('TCalendarDataSource', () { test('getSubtitle 默认返回 null', () { final dataSource = _MockDataSource(); diff --git a/tdesign-component/test/t_calendar_test.dart b/tdesign-component/test/t_calendar_test.dart index 5de19b246..7caf68ea6 100644 --- a/tdesign-component/test/t_calendar_test.dart +++ b/tdesign-component/test/t_calendar_test.dart @@ -353,6 +353,83 @@ void main() { }); }); + // ----------------------------------------------------------------------- + // initialValue / anchorRevision + // ----------------------------------------------------------------------- + group('TCalendar — initialValue 与 anchorRevision', () { + testWidgets('运行期更新 initialValue 会同步选中 UI(single)', (tester) async { + final day15 = _day(2024, 6, 15); + final day10 = _day(2024, 6, 10); + final minDate = _day(2024, 6, 1); + final maxDate = _day(2024, 6, 30); + var selected = [day15]; + var onChangeCount = 0; + + Future pumpCalendar() { + return tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + type: CalendarType.single, + initialValue: List.from(selected), + minDate: minDate, + maxDate: maxDate, + onChange: (_) => onChangeCount++, + ), + ), + ); + } + + await pumpCalendar(); + await tester.pumpAndSettle(); + + await tester.tap(find.text('15')); + await tester.pump(); + expect(onChangeCount, 0); + + selected = [day10]; + await pumpCalendar(); + await tester.pumpAndSettle(); + + final countAfterReset = onChangeCount; + await tester.tap(find.text('10')); + await tester.pump(); + expect(onChangeCount, countAfterReset); + + await tester.tap(find.text('15')); + await tester.pump(); + expect(onChangeCount, countAfterReset + 1); + }); + + testWidgets('anchorRevision 递增可重复触发锚点滚动', (tester) async { + await tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + anchorDate: _day(2024, 6, 1), + anchorRevision: 0, + minDate: _day(2024, 1, 1), + maxDate: _day(2024, 12, 31), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.pumpWidget( + _buildTestApp( + TCalendar( + height: 640, + anchorDate: _day(2024, 6, 1), + anchorRevision: 1, + minDate: _day(2024, 1, 1), + maxDate: _day(2024, 12, 31), + ), + ), + ); + await tester.pumpAndSettle(); + }); + }); + // ----------------------------------------------------------------------- // 边界条件 // ----------------------------------------------------------------------- @@ -532,7 +609,7 @@ void main() { expect(popupResult, isNull); }); - testWidgets('自定义 confirmBtn 点击后返回选中值并关闭弹窗', (tester) async { + testWidgets('自定义 confirmBtnBuilder 点击后返回选中值并关闭弹窗', (tester) async { final day15 = _day(2024, 6, 15); final day20 = _day(2024, 6, 20); List? popupResult; @@ -549,9 +626,12 @@ void main() { initialValue: [day15], minDate: _day(2024, 6, 1), maxDate: _day(2024, 6, 30), - confirmBtn: const Padding( - padding: EdgeInsets.all(16), - child: Text('ok'), + confirmBtnBuilder: (onConfirm) => Padding( + padding: const EdgeInsets.all(16), + child: GestureDetector( + onTap: onConfirm, + child: const Text('ok'), + ), ), ); }, From 49bbd3179cd9373b5f9a3bf2c94e56c44343cc9a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 08:13:43 +0000 Subject: [PATCH 31/35] [autofix.ci] apply automated fixes --- .../example/assets/api/calendar_api.md | 24 ++++--------------- tdesign-site/src/calendar/README.md | 24 ++++--------------- 2 files changed, 8 insertions(+), 40 deletions(-) diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md index d4c3e4bd9..eeb491358 100644 --- a/tdesign-component/example/assets/api/calendar_api.md +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -4,7 +4,8 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| anchorDate | DateTime? | - | 锚点日期,弹出时自动滚动到该日期所在月份。 | +| anchorDate | DateTime? | - | 锚点日期,打开时滚动到该日期所在月份。 | +| anchorRevision | int | 0 | 锚点滚动触发序号,默认 `0`。 | | animateTo | bool | false | 滚动到选中日期/锚点日期所在月份时是否使用动画,默认 false | | cellBuilder | TCalendarCellBuilder? | - | 整格自定义;设置后不再使用默认主区/副标题布局。 | | cellHeight | double? | - | 日期单元格高度,默认 60。如需更大行高可传入自定义值(如 80) | @@ -31,7 +32,7 @@ | 名称 | 返回类型 | 参数 | 说明 | | --- | --- | --- | --- | -| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget? confirmBtn, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick, bool autoClose, bool draggable, TCalendarCellBuilder? cellBuilder, TCalendarSubtitleBuilder? subtitleBuilder, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | +| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, int anchorRevision, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick, bool autoClose, bool draggable, TCalendarCellBuilder? cellBuilder, TCalendarSubtitleBuilder? subtitleBuilder, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`, 或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | ``` ``` @@ -42,8 +43,7 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | child | | - | | -| confirmBtn | Widget? | - | 自定义确认按钮(静态 Widget,需自行处理点击;推荐 [confirmBtnBuilder])。 | -| confirmBtnBuilder | Widget Function(VoidCallback onConfirm)? | - | 自定义确认按钮构建器,[onConfirm] 与默认确认按钮行为一致(回传选中值并关闭弹窗)。 | +| confirmBtnBuilder | Widget Function(VoidCallback onConfirm)? | - | 自定义确认按钮;[onConfirm] 与默认确认按钮一致(回传选中值并关闭弹窗)。 | | key | | - | | | onClose | | - | | | onConfirm | | - | | @@ -104,19 +104,3 @@ | date | | - | | | isLastDayOfMonth | | - | | | typeNotifier | | - | | - -``` -``` - -### TLunarInfo -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| day | int | - | 农历日期(数字,1-30) | -| dayText | String | - | 日期文本(如:初七) | -| isLeapMonth | bool | false | 是否是闰月 | -| month | int | - | 农历月份(数字,1-12) | -| monthText | String | - | 月份文本(如:三月、闰三月) | -| year | int | - | 农历年份(数字) | -| yearText | String | - | 年份文本(如:二〇二五) | diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md index 455a2f47f..0844ed4b2 100644 --- a/tdesign-site/src/calendar/README.md +++ b/tdesign-site/src/calendar/README.md @@ -437,7 +437,8 @@ Widget _buildLunar(BuildContext context) { | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| anchorDate | DateTime? | - | 锚点日期,弹出时自动滚动到该日期所在月份。 | +| anchorDate | DateTime? | - | 锚点日期,打开时滚动到该日期所在月份。 | +| anchorRevision | int | 0 | 锚点滚动触发序号,默认 `0`。 | | animateTo | bool | false | 滚动到选中日期/锚点日期所在月份时是否使用动画,默认 false | | cellBuilder | TCalendarCellBuilder? | - | 整格自定义;设置后不再使用默认主区/副标题布局。 | | cellHeight | double? | - | 日期单元格高度,默认 60。如需更大行高可传入自定义值(如 80) | @@ -464,7 +465,7 @@ Widget _buildLunar(BuildContext context) { | 名称 | 返回类型 | 参数 | 说明 | | --- | --- | --- | --- | -| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget? confirmBtn, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick, bool autoClose, bool draggable, TCalendarCellBuilder? cellBuilder, TCalendarSubtitleBuilder? subtitleBuilder, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | +| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, int anchorRevision, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick, bool autoClose, bool draggable, TCalendarCellBuilder? cellBuilder, TCalendarSubtitleBuilder? subtitleBuilder, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`, 或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | ``` ``` @@ -475,8 +476,7 @@ Widget _buildLunar(BuildContext context) { | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | child | | - | | -| confirmBtn | Widget? | - | 自定义确认按钮(静态 Widget,需自行处理点击;推荐 [confirmBtnBuilder])。 | -| confirmBtnBuilder | Widget Function(VoidCallback onConfirm)? | - | 自定义确认按钮构建器,[onConfirm] 与默认确认按钮行为一致(回传选中值并关闭弹窗)。 | +| confirmBtnBuilder | Widget Function(VoidCallback onConfirm)? | - | 自定义确认按钮;[onConfirm] 与默认确认按钮一致(回传选中值并关闭弹窗)。 | | key | | - | | | onClose | | - | | | onConfirm | | - | | @@ -538,21 +538,5 @@ Widget _buildLunar(BuildContext context) { | isLastDayOfMonth | | - | | | typeNotifier | | - | | -``` -``` - -### TLunarInfo -#### 默认构造方法 - -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| day | int | - | 农历日期(数字,1-30) | -| dayText | String | - | 日期文本(如:初七) | -| isLeapMonth | bool | false | 是否是闰月 | -| month | int | - | 农历月份(数字,1-12) | -| monthText | String | - | 月份文本(如:三月、闰三月) | -| year | int | - | 农历年份(数字) | -| yearText | String | - | 年份文本(如:二〇二五) | - \ No newline at end of file From ad1910427435c85308e909c0c030e7ab55d7bb49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Wed, 27 May 2026 00:21:43 +0800 Subject: [PATCH 32/35] Merge branch 'develop' into rss1102/refactor/calendar-bottom-slot --- .../workflows/issue-mark-duplicate.temp.yml | 19 - .github/workflows/issue-reply.temp.yml | 21 - .github/workflows/preview-publish.yml | 10 +- .../issue-900-tab-bar/TaskContract.md | 44 - .../issue-900-tab-bar/code-review-report.md | 47 - requirements/issue-900-tab-bar/test-cases.md | 43 - tdesign-component/coverage/lcov.info | 2418 +++++++++-------- tdesign-component/demo_tool/all_build.sh | 2 +- .../example/assets/api/action-sheet_api.md | 123 +- .../example/assets/api/avatar_api.md | 38 +- .../example/assets/api/back-top_api.md | 22 +- .../example/assets/api/badge_api.md | 37 +- .../example/assets/api/button_api.md | 130 +- .../example/assets/api/calendar_api.md | 2 +- .../example/assets/api/cascader_api.md | 10 +- .../example/assets/api/cell_api.md | 55 +- .../example/assets/api/checkbox_api.md | 93 +- .../example/assets/api/collapse_api.md | 38 +- .../example/assets/api/dialog_api.md | 112 +- .../example/assets/api/divider_api.md | 13 +- .../example/assets/api/drawer_api.md | 28 +- .../example/assets/api/dropdown-menu_api.md | 64 +- .../example/assets/api/empty_api.md | 20 +- .../example/assets/api/fab_api.md | 36 +- .../example/assets/api/footer_api.md | 15 +- .../example/assets/api/form_api.md | 46 +- .../example/assets/api/icon_api.md | 2126 ++++++++++++++- .../example/assets/api/image-viewer_api.md | 96 +- .../example/assets/api/image_api.md | 49 +- .../example/assets/api/indexes_api.md | 10 +- .../example/assets/api/input_api.md | 39 +- .../example/assets/api/link_api.md | 59 +- .../example/assets/api/loading_api.md | 28 +- .../example/assets/api/message_api.md | 40 +- .../example/assets/api/navbar_api.md | 14 +- .../example/assets/api/notice-bar_api.md | 44 +- .../example/assets/api/picker_api.md | 87 +- .../example/assets/api/popover_api.md | 81 +- .../example/assets/api/popup_api.md | 202 +- .../example/assets/api/progress_api.md | 39 +- .../assets/api/pull-down-refresh_api.md | 50 +- .../example/assets/api/radio_api.md | 98 +- .../example/assets/api/rate_api.md | 17 +- .../example/assets/api/result_api.md | 14 +- .../example/assets/api/search_api.md | 50 +- .../example/assets/api/side-bar_api.md | 16 +- .../example/assets/api/skeleton_api.md | 147 +- .../example/assets/api/slider_api.md | 32 +- .../example/assets/api/stepper_api.md | 69 +- .../example/assets/api/steps_api.md | 28 +- .../example/assets/api/swipe-cell_api.md | 41 +- .../example/assets/api/swiper_api.md | 36 +- .../example/assets/api/switch_api.md | 35 +- .../example/assets/api/tab-bar_api.md | 59 +- .../example/assets/api/table_api.md | 76 +- .../example/assets/api/tabs_api.md | 46 +- .../example/assets/api/tag_api.md | 100 +- .../example/assets/api/text_api.md | 106 +- .../example/assets/api/textarea_api.md | 12 +- .../example/assets/api/theme_api.md | 123 +- .../example/assets/api/time-counter_api.md | 74 +- .../example/assets/api/toast_api.md | 185 +- .../example/assets/api/tree-select_api.md | 22 +- .../example/assets/api/upload_api.md | 87 +- .../code/indexes._buildCustomIndexes.txt | 24 +- .../assets/code/indexes._buildOther.txt | 22 +- .../assets/code/indexes._buildSimple.txt | 22 +- .../code/picker.buildCustomItemBuilder.txt | 16 +- .../assets/code/picker.buildCustomKeys.txt | 9 +- .../assets/code/picker.buildCustomSize.txt | 3 +- .../assets/code/picker.buildCustomSlot.txt | 6 +- .../code/picker.buildGlobalDisabled.txt | 7 +- .../assets/code/picker.buildItemDisabled.txt | 13 +- .../assets/code/picker.buildLinked.txt | 10 +- .../assets/code/picker.buildSingleColumn.txt | 6 +- .../assets/code/picker.buildTimeSelect.txt | 11 +- .../assets/code/popup._buildApiDuration.txt | 22 + .../assets/code/popup._buildApiInset.txt | 23 + .../code/popup._buildApiOnOverlayClick.txt | 22 + .../code/popup._buildApiShowOverlayFalse.txt | 25 + .../assets/code/popup._buildNestedPopup.txt | 64 + .../assets/code/popup._buildPopFromBottom.txt | 18 +- .../popup._buildPopFromBottomWithClose.txt | 24 - ...uildPopFromBottomWithCloseAndLeftTitle.txt | 26 - ...p._buildPopFromBottomWithCloseAndTitle.txt | 55 +- ...popup._buildPopFromBottomWithOperation.txt | 29 - ...uildPopFromBottomWithOperationAndTitle.txt | 23 +- .../popup._buildPopFromBottomWithTitle.txt | 26 - .../assets/code/popup._buildPopFromCenter.txt | 27 +- .../popup._buildPopFromCenterWithClose.txt | 27 +- ...opup._buildPopFromCenterWithUnderClose.txt | 34 +- .../assets/code/popup._buildPopFromLeft.txt | 16 +- .../assets/code/popup._buildPopFromRight.txt | 16 +- .../assets/code/popup._buildPopFromTop.txt | 25 +- .../lib/component_test/popup_test.dart | 55 +- .../example/lib/page/t_indexes_page.dart | 68 +- .../example/lib/page/t_picker_page.dart | 132 +- .../example/lib/page/t_popup_page.dart | 1067 ++++---- .../action_sheet/t_action_sheet.dart | 125 +- .../src/components/calendar/t_calendar.dart | 150 +- .../lib/src/components/drawer/t_drawer.dart | 52 +- .../image_viewer/t_image_viewer.dart | 1 - .../lib/src/components/picker/t_picker.dart | 81 +- .../components/popup/_popup_center_close.dart | 69 + .../src/components/popup/_popup_header.dart | 177 ++ .../src/components/popup/_popup_layout.dart | 116 + .../src/components/popup/_popup_route.dart | 241 ++ .../src/components/popup/_popup_shell.dart | 106 + .../src/components/popup/_popup_tracker.dart | 25 + .../lib/src/components/popup/t_popup.dart | 85 + .../src/components/popup/t_popup_handle.dart | 173 ++ .../src/components/popup/t_popup_inset.dart | 50 + .../src/components/popup/t_popup_options.dart | 656 +++++ .../src/components/popup/t_popup_panel.dart | 622 ----- .../src/components/popup/t_popup_route.dart | 300 -- .../src/components/popup/t_popup_types.dart | 109 + .../lib/src/util/t_toolbar_pressable.dart | 96 + tdesign-component/lib/tdesign_flutter.dart | 18 +- .../test/helpers/popup_test_helpers.dart | 39 + .../test/helpers/popup_test_resource.dart | 190 ++ .../test/t_bottom_tab_bar_test.dart | 42 +- tdesign-component/test/t_picker_test.dart | 121 +- .../test/t_popup_coverage_test.dart | 1238 +++++++++ .../test/t_popup_layout_test.dart | 208 ++ .../test/t_popup_options_test.dart | 389 +++ .../test/t_popup_route_test.dart | 177 ++ tdesign-component/test/t_popup_test.dart | 1507 ++++++++++ tdesign-site/.husky/commit-msg | 4 - tdesign-site/.husky/pre-commit | 4 - tdesign-site/.husky/prepare-commit-msg | 4 - .../$$var_template/$$var_filename.js | 1 - .../$$var_template/$$var_filename.json | 6 - .../$$var_template/$$var_filename.wxml | 1 - .../$$var_template/$$var_filename.wxss | 0 tdesign-site/.templates/.editorconfig | 21 - .../.templates/_example/$$var_filename.js | 1 - .../.templates/_example/$$var_filename.json | 3 - .../.templates/_example/$$var_filename.wxml | 3 - .../.templates/_example/$$var_filename.wxss | 0 tdesign-site/.vscode/component.code-snippets | 14 - tdesign-site/.vscode/launch.json | 14 - tdesign-site/.vscode/new.code-snippets | 37 - tdesign-site/.vscode/settings.json | 36 - tdesign-site/.vscode/test.code-snippets | 31 - tdesign-site/commitlint.config.js | 1 - tdesign-site/package.json | 20 - tdesign-site/pnpm-lock.yaml | 896 +----- tdesign-site/src/action-sheet/README.md | 123 +- tdesign-site/src/avatar/README.md | 38 +- tdesign-site/src/back-top/README.md | 22 +- tdesign-site/src/badge/README.md | 37 +- tdesign-site/src/button/README.md | 130 +- tdesign-site/src/calendar/README.md | 7 +- tdesign-site/src/cascader/README.md | 10 +- tdesign-site/src/cell/README.md | 55 +- tdesign-site/src/checkbox/README.md | 93 +- tdesign-site/src/collapse/README.md | 38 +- tdesign-site/src/dialog/README.md | 112 +- tdesign-site/src/divider/README.md | 13 +- tdesign-site/src/drawer/README.md | 28 +- tdesign-site/src/dropdown-menu/README.md | 65 +- tdesign-site/src/empty/README.md | 20 +- tdesign-site/src/fab/README.md | 36 +- tdesign-site/src/footer/README.md | 15 +- tdesign-site/src/form/README.md | 46 +- tdesign-site/src/image-viewer/README.md | 96 +- tdesign-site/src/image/README.md | 49 +- tdesign-site/src/indexes/README.md | 98 +- tdesign-site/src/input/README.md | 39 +- tdesign-site/src/link/README.md | 59 +- tdesign-site/src/loading/README.md | 28 +- tdesign-site/src/message/README.md | 40 +- tdesign-site/src/navbar/README.md | 14 +- tdesign-site/src/notice-bar/README.md | 44 +- tdesign-site/src/picker/README.md | 166 +- tdesign-site/src/popover/README.md | 81 +- tdesign-site/src/popup/README.md | 830 ++++-- tdesign-site/src/progress/README.md | 39 +- tdesign-site/src/pull-down-refresh/README.md | 50 +- tdesign-site/src/radio/README.md | 98 +- tdesign-site/src/rate/README.md | 17 +- tdesign-site/src/result/README.md | 14 +- tdesign-site/src/search/README.md | 50 +- tdesign-site/src/side-bar/README.md | 16 +- tdesign-site/src/slider/README.md | 32 +- tdesign-site/src/stepper/README.md | 69 +- tdesign-site/src/steps/README.md | 28 +- tdesign-site/src/swipe-cell/README.md | 41 +- tdesign-site/src/swiper/README.md | 36 +- tdesign-site/src/switch/README.md | 35 +- tdesign-site/src/tab-bar/README.md | 59 +- tdesign-site/src/table/README.md | 76 +- tdesign-site/src/tabs/README.md | 46 +- tdesign-site/src/tag/README.md | 100 +- tdesign-site/src/text/README.md | 106 +- tdesign-site/src/textarea/README.md | 12 +- tdesign-site/src/time-counter/README.md | 74 +- tdesign-site/src/toast/README.md | 183 +- tdesign-site/src/tree-select/README.md | 22 +- tdesign-site/src/upload/README.md | 87 +- tdesign-site/template.config.js | 34 - 201 files changed, 15994 insertions(+), 5918 deletions(-) delete mode 100644 .github/workflows/issue-mark-duplicate.temp.yml delete mode 100644 .github/workflows/issue-reply.temp.yml delete mode 100644 requirements/issue-900-tab-bar/TaskContract.md delete mode 100644 requirements/issue-900-tab-bar/code-review-report.md delete mode 100644 requirements/issue-900-tab-bar/test-cases.md create mode 100644 tdesign-component/example/assets/code/popup._buildApiDuration.txt create mode 100644 tdesign-component/example/assets/code/popup._buildApiInset.txt create mode 100644 tdesign-component/example/assets/code/popup._buildApiOnOverlayClick.txt create mode 100644 tdesign-component/example/assets/code/popup._buildApiShowOverlayFalse.txt create mode 100644 tdesign-component/example/assets/code/popup._buildNestedPopup.txt delete mode 100644 tdesign-component/example/assets/code/popup._buildPopFromBottomWithClose.txt delete mode 100644 tdesign-component/example/assets/code/popup._buildPopFromBottomWithCloseAndLeftTitle.txt delete mode 100644 tdesign-component/example/assets/code/popup._buildPopFromBottomWithOperation.txt delete mode 100644 tdesign-component/example/assets/code/popup._buildPopFromBottomWithTitle.txt create mode 100644 tdesign-component/lib/src/components/popup/_popup_center_close.dart create mode 100644 tdesign-component/lib/src/components/popup/_popup_header.dart create mode 100644 tdesign-component/lib/src/components/popup/_popup_layout.dart create mode 100644 tdesign-component/lib/src/components/popup/_popup_route.dart create mode 100644 tdesign-component/lib/src/components/popup/_popup_shell.dart create mode 100644 tdesign-component/lib/src/components/popup/_popup_tracker.dart create mode 100644 tdesign-component/lib/src/components/popup/t_popup.dart create mode 100644 tdesign-component/lib/src/components/popup/t_popup_handle.dart create mode 100644 tdesign-component/lib/src/components/popup/t_popup_inset.dart create mode 100644 tdesign-component/lib/src/components/popup/t_popup_options.dart delete mode 100644 tdesign-component/lib/src/components/popup/t_popup_panel.dart delete mode 100644 tdesign-component/lib/src/components/popup/t_popup_route.dart create mode 100644 tdesign-component/lib/src/components/popup/t_popup_types.dart create mode 100644 tdesign-component/lib/src/util/t_toolbar_pressable.dart create mode 100644 tdesign-component/test/helpers/popup_test_helpers.dart create mode 100644 tdesign-component/test/helpers/popup_test_resource.dart create mode 100644 tdesign-component/test/t_popup_coverage_test.dart create mode 100644 tdesign-component/test/t_popup_layout_test.dart create mode 100644 tdesign-component/test/t_popup_options_test.dart create mode 100644 tdesign-component/test/t_popup_route_test.dart create mode 100644 tdesign-component/test/t_popup_test.dart delete mode 100755 tdesign-site/.husky/commit-msg delete mode 100755 tdesign-site/.husky/pre-commit delete mode 100755 tdesign-site/.husky/prepare-commit-msg delete mode 100644 tdesign-site/.templates/$$var_template/$$var_filename.js delete mode 100644 tdesign-site/.templates/$$var_template/$$var_filename.json delete mode 100644 tdesign-site/.templates/$$var_template/$$var_filename.wxml delete mode 100644 tdesign-site/.templates/$$var_template/$$var_filename.wxss delete mode 100644 tdesign-site/.templates/.editorconfig delete mode 100644 tdesign-site/.templates/_example/$$var_filename.js delete mode 100644 tdesign-site/.templates/_example/$$var_filename.json delete mode 100644 tdesign-site/.templates/_example/$$var_filename.wxml delete mode 100644 tdesign-site/.templates/_example/$$var_filename.wxss delete mode 100644 tdesign-site/.vscode/component.code-snippets delete mode 100644 tdesign-site/.vscode/launch.json delete mode 100644 tdesign-site/.vscode/new.code-snippets delete mode 100644 tdesign-site/.vscode/settings.json delete mode 100644 tdesign-site/.vscode/test.code-snippets delete mode 100644 tdesign-site/commitlint.config.js delete mode 100644 tdesign-site/template.config.js diff --git a/.github/workflows/issue-mark-duplicate.temp.yml b/.github/workflows/issue-mark-duplicate.temp.yml deleted file mode 100644 index 301772324..000000000 --- a/.github/workflows/issue-mark-duplicate.temp.yml +++ /dev/null @@ -1,19 +0,0 @@ -# force copy from tencent/tdesign -# 当在 issue 的 comment 回复类似 `Duplicate of #111` 这样的话,issue 将被自动打上 重复提交标签 并且 cloese -name: Issue Mark Duplicate - -on: - issue_comment: - types: [created, edited] - -jobs: - mark-duplicate: - runs-on: ubuntu-latest - steps: - - name: mark-duplicate - uses: actions-cool/issues-helper@v2 - with: - actions: "mark-duplicate" - token: ${{ secrets.GITHUB_TOKEN }} - duplicate-labels: "duplicate" - close-issue: true diff --git a/.github/workflows/issue-reply.temp.yml b/.github/workflows/issue-reply.temp.yml deleted file mode 100644 index 271a94f53..000000000 --- a/.github/workflows/issue-reply.temp.yml +++ /dev/null @@ -1,21 +0,0 @@ -# force copy from tencent/tdesign -# 当被打上 Need Reproduce 标签时候,自动提示需要重现实例 - -name: ISSUE_REPLY - -on: - issues: - types: [labeled] - -jobs: - issue-reply: - runs-on: ubuntu-latest - steps: - - name: Need Reproduce - if: github.event.label.name == 'Need Reproduce' - uses: actions-cool/issues-helper@v2 - with: - actions: 'create-comment' - issue-number: ${{ github.event.issue.number }} - body: | - 你好 @${{ github.event.issue.user.login }}, 我们需要你提供一个在线的重现实例以便于我们帮你排查问题。你可以通过点击 [此处](https://codesandbox.io/) 创建一个 codesandbox 或者提供一个最小化的 GitHub 仓库。请确保选择准确的版本。 diff --git a/.github/workflows/preview-publish.yml b/.github/workflows/preview-publish.yml index f2f69f964..c1f50fcfa 100644 --- a/.github/workflows/preview-publish.yml +++ b/.github/workflows/preview-publish.yml @@ -8,6 +8,10 @@ on: types: - completed +permissions: + actions: read + pull-requests: write + jobs: preview-success-web: runs-on: ubuntu-latest @@ -74,7 +78,7 @@ jobs: echo "APK download URL: $APK_URL" - name: update status comment - uses: actions-cool/maintain-one-comment@v3 + uses: TDesignOteam/workflows/actions/maintain-one-comment@main with: token: ${{ secrets.GITHUB_TOKEN }} body: | @@ -90,7 +94,7 @@ jobs: - name: The job failed if: ${{ failure() }} - uses: actions-cool/maintain-one-comment@v3 + uses: TDesignOteam/workflows/actions/maintain-one-comment@main with: token: ${{ secrets.GITHUB_TOKEN }} body: | @@ -122,7 +126,7 @@ jobs: done < build-env.txt - name: The job failed - uses: actions-cool/maintain-one-comment@v3 + uses: TDesignOteam/workflows/actions/maintain-one-comment@main with: token: ${{ secrets.GITHUB_TOKEN }} body: | diff --git a/requirements/issue-900-tab-bar/TaskContract.md b/requirements/issue-900-tab-bar/TaskContract.md deleted file mode 100644 index c113ef934..000000000 --- a/requirements/issue-900-tab-bar/TaskContract.md +++ /dev/null @@ -1,44 +0,0 @@ -# TaskContract — issue #900 TabBar 图标选中不变色 - -## 基本信息 - -- issue: https://github.com/Tencent/tdesign-flutter/issues/900 -- 组件:`TBottomTabBar` -- 分支:`fix/issue-900-tab-bar` -- 优先级:P2(功能缺陷,影响视觉反馈) - -## 问题描述 - -`TBottomTabBarBasicType.iconText` 类型下,切换选中 tab 时: -- 文字颜色正确切换(选中:brandNormalColor,未选中:textColorPrimary) -- **图标颜色不切换**,始终保持同一颜色 - -## 根因分析 - -`_constructItem` 方法中: - -- `text` 类型:通过 `_textItem` 内的 `textColor` 参数正确应用了选中/未选中颜色 -- `icon` / `iconText` 类型:仅切换 `selectedIcon` / `unselectedIcon` widget,**未包裹 `IconTheme`**,图标颜色由 widget 自身决定,不受选中状态控制 - -## 修复方案 - -在 `icon` 和 `iconText` 分支中,用 `IconTheme` 包裹图标 widget,根据 `isSelected` 注入对应颜色: -- 选中:`TTheme.of(context).brandNormalColor` -- 未选中:`TTheme.of(context).textColorPrimary` - -## 交付物清单 - -| 序号 | 交付物 | 负责角色 | -|------|--------|---------| -| 1 | `requirements/issue-900-tab-bar/test-cases.md` | tester | -| 2 | `tdesign-component/test/t_bottom_tab_bar_test.dart` | tester | -| 3 | `tdesign-component/lib/src/components/tabbar/t_bottom_tab_bar.dart`(修复) | developer | -| 4 | `requirements/issue-900-tab-bar/code-review-report.md` | reviewer | -| 5 | `requirements/issue-900-tab-bar/evaluation-report.md` | tester | -| 6 | PR to develop | ci | - -## 确认点 - -1. 修复后 `icon` 类型也需同步修复(同样逻辑缺陷) -2. 如果用户自定义图标显式设置了 `color`,`IconTheme` 不应覆盖(Flutter 的 `Icon` widget 显式 color 优先级高于 IconTheme,无需额外处理) -3. 不改动任何公开 API diff --git a/requirements/issue-900-tab-bar/code-review-report.md b/requirements/issue-900-tab-bar/code-review-report.md deleted file mode 100644 index 5065ce987..000000000 --- a/requirements/issue-900-tab-bar/code-review-report.md +++ /dev/null @@ -1,47 +0,0 @@ -# Code Review Report — issue #900 - -## 审查结论:✅ 通过 - -## 修改范围 - -文件:`tdesign-component/lib/src/components/tabbar/t_bottom_tab_bar.dart` -方法:`_constructItem`(`TBottomTabBarItemWithBadge`) - -## 改动评审 - -### 正确性 ✅ - -- `icon` 和 `iconText` 两个分支均已添加 `IconTheme` 包裹 -- 颜色逻辑与 `_textItem` 的文字颜色逻辑保持一致: - - 选中 → `brandNormalColor` - - 未选中 → `textColorPrimary` -- Flutter 的 `Icon` widget 在显式设置 `color` 时优先级高于 `IconTheme`,TC-07 边界场景天然满足,无需额外处理 - -### API 一致性 ✅ - -- 未改动任何公开 API(`TBottomTabBarTabConfig`、`TBottomTabBar` 构造函数) -- 向后兼容:原有使用 `selectedIcon`/`unselectedIcon` 传入不同颜色图标的用法不受影响 - -### 色值规范 ✅ - -- 颜色均从 `TTheme.of(context)` 取,未硬编码 - -### 测试覆盖 ✅ - -- TC-01 ~ TC-07 覆盖:选中/未选中/切换/回归/边界场景 -- `icon` 类型和 `iconText` 类型均有对应测试用例 - -### 回归风险评估 🟡 低风险 - -- `expansionPanel` 类型中已有图标(`TIcons.view_list`)也未加 `IconTheme`,但该类型的图标颜色逻辑是硬编码在 `isSelected ? brandNormalColor : textColorPrimary` 三元里的(第 710 行),**颜色已正确,不受本次修复影响** -- `text` 类型无图标,不受影响 - -## 不需要改动的部分 - -- `expansionPanel` 分支(已用三元直接设 `color`,行为正确) -- `_textItem`(文字颜色逻辑原本就正确) -- 任何公开 API - -## 结论 - -本次修复最小化、精准、无 API 破坏。批准合并。 diff --git a/requirements/issue-900-tab-bar/test-cases.md b/requirements/issue-900-tab-bar/test-cases.md deleted file mode 100644 index 11579d4ea..000000000 --- a/requirements/issue-900-tab-bar/test-cases.md +++ /dev/null @@ -1,43 +0,0 @@ -# 测试用例 — issue #900 TabBar 图标选中颜色 - -## TC-01:iconText 类型 — 选中 tab 图标颜色为 brandNormalColor - -- **前置条件**:`TBottomTabBar` 使用 `iconText` 类型,`currentIndex=0` -- **操作**:渲染组件,读取 index=0 的图标颜色 -- **期望**:图标颜色 == `TTheme.brandNormalColor` - -## TC-02:iconText 类型 — 未选中 tab 图标颜色为 textColorPrimary - -- **前置条件**:`TBottomTabBar` 使用 `iconText` 类型,`currentIndex=0` -- **操作**:渲染组件,读取 index=1 的图标颜色 -- **期望**:图标颜色 == `TTheme.textColorPrimary` - -## TC-03:iconText 类型 — 点击切换后图标颜色同步更新 - -- **前置条件**:`TBottomTabBar` 使用 `iconText` 类型,初始 `currentIndex=0` -- **操作**:点击 index=1 的 tab -- **期望**:index=1 图标颜色变为 `brandNormalColor`,index=0 图标颜色变为 `textColorPrimary` - -## TC-04:icon 类型 — 选中 tab 图标颜色为 brandNormalColor - -- **前置条件**:`TBottomTabBar` 使用 `icon` 类型,`currentIndex=0` -- **操作**:渲染组件,读取 index=0 的图标颜色 -- **期望**:图标颜色 == `TTheme.brandNormalColor` - -## TC-05:icon 类型 — 未选中 tab 图标颜色为 textColorPrimary - -- **前置条件**:`TBottomTabBar` 使用 `icon` 类型,`currentIndex=0` -- **操作**:渲染组件,读取 index=1 的图标颜色 -- **期望**:图标颜色 == `TTheme.textColorPrimary` - -## TC-06:iconText 类型 — 文字颜色不受影响(回归) - -- **前置条件**:`TBottomTabBar` 使用 `iconText` 类型,`currentIndex=0` -- **操作**:渲染组件,读取 index=0 和 index=1 的文字颜色 -- **期望**:选中文字颜色为 `brandNormalColor`,未选中为 `textColorPrimary`(与修复前一致) - -## TC-07:用户显式设置图标 color 时不被覆盖(边界) - -- **前置条件**:`selectedIcon: Icon(TIcons.book, color: Colors.red)` -- **操作**:渲染组件,读取图标颜色 -- **期望**:图标颜色为 `Colors.red`(用户显式颜色优先,IconTheme 不覆盖) diff --git a/tdesign-component/coverage/lcov.info b/tdesign-component/coverage/lcov.info index 687912903..018b00e48 100644 --- a/tdesign-component/coverage/lcov.info +++ b/tdesign-component/coverage/lcov.info @@ -1,36 +1,62 @@ -SF:lib/src/components/picker/t_picker_normalize.dart -DA:9,0 -DA:12,1 -DA:14,1 -DA:17,1 -DA:18,1 -DA:19,1 -DA:20,3 -DA:22,1 -DA:27,1 -DA:29,1 -DA:32,1 -DA:35,1 -DA:36,1 -DA:39,1 -DA:40,3 -DA:41,2 -DA:42,2 -DA:43,1 -DA:44,1 -DA:46,2 -DA:49,1 -DA:51,1 -DA:52,2 -DA:53,1 -DA:54,2 -DA:59,1 -DA:60,1 -DA:61,1 -DA:63,1 -DA:64,3 -LF:30 -LH:29 +SF:lib/src/components/popup/t_popup_config.dart +DA:7,4 +DA:47,4 +DA:85,4 +DA:86,4 +DA:89,4 +DA:175,4 +DA:176,8 +DA:177,8 +DA:180,4 +DA:181,8 +DA:182,8 +DA:185,4 +DA:186,12 +DA:189,4 +DA:190,8 +DA:191,8 +DA:192,8 +DA:195,4 +DA:196,8 +DA:197,4 +DA:198,8 +DA:200,6 +DA:203,2 +DA:204,4 +DA:205,4 +DA:206,2 +DA:207,2 +DA:208,7 +DA:210,2 +DA:211,4 +DA:212,2 +DA:213,6 +DA:215,4 +DA:216,8 +DA:217,4 +DA:218,4 +DA:219,4 +DA:220,2 +DA:221,2 +DA:222,2 +DA:226,4 +DA:227,5 +DA:228,4 +DA:229,4 +DA:233,3 +DA:234,3 +DA:235,3 +DA:236,2 +DA:237,2 +DA:242,8 +DA:243,3 +DA:244,3 +DA:245,3 +DA:246,3 +DA:247,2 +DA:252,4 +LF:56 +LH:56 end_of_record SF:lib/src/components/action_sheet/t_action_sheet.dart DA:20,0 @@ -66,140 +92,134 @@ DA:246,0 DA:250,0 DA:251,0 DA:254,0 -DA:257,0 -DA:262,0 -DA:287,0 +DA:256,0 +DA:260,0 +DA:281,0 +DA:285,0 DA:289,0 -DA:293,0 -DA:295,0 -DA:296,0 -DA:306,0 -DA:307,0 -DA:323,0 -DA:324,0 -DA:340,0 -DA:342,0 -LF:46 +DA:290,0 +DA:301,0 +DA:302,0 +DA:319,0 +DA:320,0 +DA:334,0 +LF:44 LH:0 end_of_record SF:lib/src/util/context_extension.dart -DA:7,0 +DA:7,9 LF:1 -LH:0 +LH:1 end_of_record -SF:lib/src/components/popup/t_popup_route.dart -DA:14,0 -DA:74,0 -DA:76,0 -DA:79,0 -DA:82,0 -DA:83,0 -DA:88,0 -DA:89,0 -DA:101,0 -DA:108,0 -DA:109,0 -DA:110,0 -DA:111,0 -DA:112,0 -DA:114,0 -DA:116,0 -DA:117,0 -DA:118,0 -DA:119,0 -DA:120,0 -DA:121,0 -DA:122,0 -DA:125,0 -DA:126,0 -DA:127,0 -DA:128,0 -DA:135,0 -DA:137,0 -DA:138,0 -DA:139,0 -DA:140,0 -DA:141,0 -DA:142,0 -DA:143,0 -DA:147,0 -DA:148,0 -DA:158,0 -DA:160,0 -DA:161,0 -DA:166,0 -DA:168,0 -DA:169,0 -DA:170,0 -DA:171,0 -DA:172,0 -DA:175,0 -DA:178,0 -DA:180,0 -DA:181,0 -DA:185,0 -DA:186,0 -DA:190,0 -DA:191,0 -DA:194,0 -DA:196,0 -DA:197,0 -DA:198,0 -DA:199,0 -DA:200,0 -DA:201,0 -DA:204,0 -DA:207,0 -DA:209,0 -DA:210,0 -DA:213,0 -DA:215,0 -DA:216,0 -DA:217,0 -DA:219,0 -DA:220,0 -DA:221,0 -DA:223,0 -DA:224,0 -DA:229,0 -DA:230,0 -DA:231,0 -DA:232,0 -DA:233,0 -DA:234,0 -DA:236,0 -DA:238,0 -DA:243,0 -DA:245,0 -DA:246,0 -DA:247,0 -DA:248,0 -DA:249,0 -DA:250,0 -DA:251,0 -DA:252,0 -DA:259,0 -DA:261,0 -DA:263,0 -DA:265,0 -DA:267,0 -DA:269,0 -DA:278,0 -DA:280,0 -DA:282,0 -DA:283,0 -DA:284,0 -DA:285,0 -DA:286,0 -DA:287,0 -DA:288,0 -DA:289,0 -DA:290,0 -DA:292,0 -DA:296,0 -DA:298,0 -LF:110 -LH:0 +SF:lib/src/components/popup/t_popup.dart +DA:19,1 +DA:187,3 +DA:228,3 +DA:266,3 +DA:269,3 +DA:274,3 +DA:276,2 +DA:277,4 +DA:284,3 +DA:285,3 +DA:288,3 +DA:289,3 +DA:290,3 +DA:293,3 +DA:298,3 +DA:303,3 +DA:305,9 +DA:306,3 +DA:307,3 +DA:316,3 +DA:317,3 +DA:318,3 +DA:319,6 +DA:320,3 +DA:324,1 +DA:325,1 +DA:331,1 +DA:333,1 +DA:334,2 +DA:335,4 +DA:339,1 +DA:341,2 +DA:342,1 +DA:345,1 +DA:346,2 +DA:349,2 +DA:350,1 +DA:351,3 +DA:352,2 +DA:353,2 +DA:354,2 +DA:355,2 +DA:356,2 +DA:357,2 +DA:358,2 +DA:359,2 +DA:360,2 +DA:361,2 +DA:362,2 +DA:363,2 +DA:364,2 +DA:365,2 +DA:366,2 +DA:367,2 +DA:368,2 +DA:369,2 +DA:370,2 +DA:371,2 +DA:372,2 +DA:373,2 +DA:374,2 +DA:375,2 +DA:376,2 +DA:377,2 +DA:378,2 +DA:379,2 +DA:380,2 +DA:381,2 +DA:382,2 +DA:383,2 +DA:384,2 +DA:385,2 +DA:386,2 +DA:387,2 +DA:388,2 +DA:392,1 +DA:394,2 +LF:77 +LH:77 +end_of_record +SF:lib/src/components/popup/t_popup_tracker.dart +DA:5,9 +DA:7,3 +DA:8,15 +DA:11,3 +DA:12,9 +DA:13,9 +DA:14,6 +DA:18,3 +DA:19,6 +DA:20,3 +DA:23,3 +LF:11 +LH:11 +end_of_record +SF:lib/src/components/popup/t_popup_handle.dart +DA:5,3 +DA:18,9 +DA:23,3 +DA:24,3 +DA:27,6 +DA:30,3 +DA:31,3 +DA:34,3 +DA:35,3 +DA:36,3 +LF:10 +LH:10 end_of_record SF:lib/src/components/action_sheet/t_action_sheet_grid.dart DA:30,0 @@ -483,7 +503,7 @@ DA:48,0 DA:51,0 DA:54,0 DA:57,0 -DA:60,3 +DA:60,9 DA:63,0 DA:68,0 DA:71,0 @@ -533,11 +553,11 @@ DA:204,0 DA:207,0 DA:210,0 DA:213,0 -DA:218,3 -DA:221,3 +DA:218,0 +DA:221,0 DA:224,0 DA:227,0 -DA:230,0 +DA:230,6 DA:233,0 DA:236,0 DA:239,0 @@ -557,11 +577,11 @@ DA:280,0 DA:283,0 DA:286,0 DA:290,0 -DA:292,0 +DA:292,9 DA:294,0 DA:296,0 DA:298,0 -DA:300,3 +DA:300,0 DA:302,0 DA:303,0 DA:305,0 @@ -572,15 +592,15 @@ DA:312,0 DA:314,0 DA:316,0 DA:318,0 -DA:322,3 -DA:324,0 -DA:326,3 -DA:328,3 +DA:322,9 +DA:324,9 +DA:326,0 +DA:328,0 DA:330,0 DA:332,0 DA:334,0 LF:110 -LH:7 +LH:5 end_of_record SF:lib/src/theme/t_fonts.dart DA:8,0 @@ -589,11 +609,11 @@ DA:14,0 DA:17,0 DA:20,0 DA:23,0 -DA:26,0 -DA:29,3 +DA:26,9 +DA:29,9 DA:32,0 DA:35,0 -DA:38,3 +DA:38,9 DA:41,0 DA:44,0 DA:47,0 @@ -605,13 +625,13 @@ DA:62,0 DA:65,0 DA:68,0 LF:21 -LH:2 +LH:3 end_of_record SF:lib/src/theme/t_radius.dart DA:8,0 -DA:9,3 +DA:9,0 DA:10,0 -DA:11,0 +DA:11,9 DA:14,0 DA:17,0 LF:6 @@ -619,45 +639,45 @@ LH:1 end_of_record SF:lib/src/theme/t_spacers.dart DA:5,0 -DA:7,3 -DA:9,3 +DA:7,9 +DA:9,9 DA:11,3 DA:13,0 -DA:15,3 +DA:15,0 DA:17,0 DA:19,0 DA:21,0 DA:23,0 DA:25,0 LF:11 -LH:4 +LH:3 end_of_record SF:lib/src/theme/t_theme.dart -DA:13,0 -DA:18,0 -DA:35,0 -DA:38,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 +DA:13,3 +DA:18,3 +DA:35,3 +DA:38,3 +DA:40,6 +DA:41,3 +DA:42,3 +DA:43,3 +DA:44,3 DA:48,0 -DA:55,0 -DA:59,0 +DA:55,3 +DA:59,6 DA:63,0 DA:64,0 -DA:69,1 -DA:72,1 +DA:69,3 +DA:72,0 DA:76,0 DA:77,0 DA:79,0 DA:80,0 DA:86,0 DA:89,0 -DA:136,1 -DA:149,1 -DA:152,1 +DA:136,3 +DA:149,3 +DA:152,3 DA:158,0 DA:159,0 DA:167,0 @@ -695,64 +715,64 @@ DA:245,0 DA:247,0 DA:248,0 DA:251,0 -DA:257,1 -DA:261,1 -DA:262,1 -DA:264,1 -DA:265,1 -DA:266,1 -DA:268,1 -DA:269,1 -DA:270,4 -DA:284,1 -DA:291,1 +DA:257,3 +DA:261,3 +DA:262,3 +DA:264,3 +DA:265,3 +DA:266,3 +DA:268,3 +DA:269,3 +DA:270,12 +DA:284,3 +DA:291,3 DA:292,0 -DA:297,1 -DA:298,1 -DA:299,1 -DA:300,1 +DA:297,3 +DA:298,3 +DA:299,3 +DA:300,3 DA:301,0 -DA:302,1 -DA:304,1 -DA:305,1 -DA:306,1 -DA:308,3 -DA:309,2 +DA:302,3 +DA:304,3 +DA:305,3 +DA:306,3 +DA:308,9 +DA:309,6 DA:332,0 DA:333,0 DA:337,0 -DA:342,1 -DA:343,1 -DA:344,2 -DA:345,1 -DA:350,1 -DA:351,2 -DA:352,1 -DA:354,2 -DA:359,1 -DA:360,2 -DA:361,2 -DA:365,1 -DA:366,2 -DA:367,3 -DA:371,1 -DA:372,2 -DA:373,3 -DA:377,1 -DA:378,2 -DA:379,3 -DA:383,1 -DA:384,2 -DA:385,1 -DA:386,2 -DA:387,2 -DA:388,2 -DA:389,2 -DA:390,2 -DA:391,4 -DA:392,3 -DA:396,2 -DA:400,1 +DA:342,3 +DA:343,3 +DA:344,6 +DA:345,3 +DA:350,3 +DA:351,6 +DA:352,3 +DA:354,6 +DA:359,3 +DA:360,6 +DA:361,6 +DA:365,3 +DA:366,6 +DA:367,9 +DA:371,3 +DA:372,6 +DA:373,9 +DA:377,3 +DA:378,6 +DA:379,9 +DA:383,3 +DA:384,6 +DA:385,3 +DA:386,6 +DA:387,6 +DA:388,6 +DA:389,6 +DA:390,6 +DA:391,12 +DA:392,9 +DA:396,6 +DA:400,3 DA:401,0 DA:402,0 DA:406,0 @@ -781,17 +801,17 @@ DA:456,0 DA:457,0 DA:458,0 DA:459,0 -DA:474,1 -DA:477,2 -DA:481,1 -DA:484,2 -DA:485,1 -DA:489,3 -DA:490,1 -DA:496,1 -DA:497,1 +DA:474,3 +DA:477,6 +DA:481,3 +DA:484,6 +DA:485,3 +DA:489,9 +DA:490,3 +DA:496,3 +DA:497,3 LF:157 -LH:67 +LH:77 end_of_record SF:lib/src/util/iterable_ext.dart DA:8,0 @@ -842,7 +862,7 @@ LF:14 LH:0 end_of_record SF:lib/src/components/badge/t_badge.dart -DA:43,1 +DA:43,5 DA:57,0 DA:95,0 DA:96,0 @@ -966,15 +986,15 @@ LF:120 LH:1 end_of_record SF:lib/src/components/text/t_text.dart -DA:41,1 -DA:68,1 +DA:41,3 +DA:68,3 DA:71,0 DA:98,0 -DA:163,1 -DA:165,1 +DA:163,3 +DA:165,3 DA:167,0 DA:169,0 -DA:172,1 +DA:172,3 DA:173,0 DA:174,0 DA:176,0 @@ -992,68 +1012,68 @@ DA:192,0 DA:193,0 DA:194,0 DA:196,0 -DA:199,3 -DA:201,1 +DA:199,6 +DA:201,3 DA:203,0 DA:205,0 -DA:210,1 -DA:211,1 -DA:214,1 -DA:216,1 -DA:217,2 +DA:210,3 +DA:211,3 +DA:214,3 +DA:216,3 +DA:217,0 DA:218,0 -DA:220,2 -DA:221,3 -DA:223,1 +DA:220,6 +DA:221,6 +DA:223,3 DA:224,0 DA:225,0 -DA:227,2 -DA:229,1 +DA:227,6 +DA:229,3 DA:230,0 DA:232,0 -DA:237,2 -DA:238,1 -DA:239,2 -DA:244,2 -DA:245,2 -DA:246,2 -DA:247,2 -DA:248,2 -DA:249,2 -DA:250,3 -DA:251,2 -DA:252,2 -DA:253,2 -DA:254,2 -DA:255,2 -DA:256,2 -DA:257,2 -DA:258,1 -DA:259,3 -DA:260,2 -DA:261,2 -DA:262,2 -DA:265,2 -DA:266,1 +DA:237,6 +DA:238,3 +DA:239,3 +DA:244,6 +DA:245,9 +DA:246,3 +DA:247,3 +DA:248,3 +DA:249,3 +DA:250,6 +DA:251,3 +DA:252,3 +DA:253,3 +DA:254,3 +DA:255,3 +DA:256,3 +DA:257,3 +DA:258,3 +DA:259,6 +DA:260,3 +DA:261,3 +DA:262,3 +DA:265,3 +DA:266,3 DA:272,0 DA:273,0 -DA:276,1 -DA:280,1 -DA:281,1 -DA:282,1 -DA:283,1 -DA:285,1 -DA:286,1 -DA:287,1 -DA:288,1 -DA:289,1 -DA:290,1 -DA:291,1 -DA:292,1 -DA:293,1 -DA:294,1 -DA:295,1 -DA:296,1 +DA:276,3 +DA:280,3 +DA:281,3 +DA:282,3 +DA:283,3 +DA:285,3 +DA:286,3 +DA:287,3 +DA:288,3 +DA:289,3 +DA:290,3 +DA:291,3 +DA:292,3 +DA:293,3 +DA:294,3 +DA:295,3 +DA:296,3 DA:298,0 DA:299,0 DA:301,0 @@ -1141,7 +1161,7 @@ DA:497,0 DA:502,0 DA:505,0 LF:174 -LH:58 +LH:57 end_of_record SF:lib/src/components/action_sheet/t_action_sheet_item_widget.dart DA:12,0 @@ -1850,96 +1870,92 @@ LF:104 LH:0 end_of_record SF:lib/src/components/picker/no_wave_behavior.dart -DA:11,1 -DA:14,2 -DA:17,1 -DA:21,1 -DA:23,1 -DA:24,1 -DA:25,1 -DA:26,1 -DA:27,1 -DA:28,1 +DA:11,0 +DA:14,0 +DA:17,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 LF:10 -LH:10 +LH:0 end_of_record SF:lib/src/components/picker/t_item_widget.dart -DA:31,1 -DA:67,1 -DA:68,1 -DA:70,1 -DA:72,1 -DA:74,1 -DA:75,1 -DA:76,1 -DA:78,1 -DA:79,1 -DA:82,1 -DA:84,1 -DA:85,2 -DA:92,1 -DA:94,1 -DA:95,1 -DA:96,1 -DA:98,7 -DA:99,1 -DA:100,1 -DA:101,1 -DA:102,2 -DA:104,1 -DA:105,1 -DA:106,1 -DA:110,1 -DA:111,1 -DA:114,1 -DA:115,1 -DA:116,1 -DA:117,1 -DA:133,1 -DA:150,3 -DA:153,1 -DA:154,1 -DA:155,1 -DA:156,1 -DA:157,2 -DA:158,1 -DA:161,1 -DA:164,1 -DA:168,1 -DA:169,2 -DA:172,1 -DA:173,3 -DA:174,3 -DA:178,1 -DA:179,2 +DA:31,0 +DA:67,0 +DA:68,0 +DA:70,0 +DA:72,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:78,0 +DA:79,0 +DA:82,0 +DA:84,0 +DA:85,0 +DA:92,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:110,0 +DA:111,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:133,5 +DA:150,0 +DA:153,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:161,0 +DA:164,0 +DA:168,0 +DA:169,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:178,0 +DA:179,0 LF:48 -LH:48 +LH:1 end_of_record SF:lib/src/components/picker/t_picker_option.dart -DA:14,2 -DA:29,1 -DA:32,1 -DA:33,3 -DA:34,3 -DA:35,3 -DA:36,3 -DA:38,1 -DA:39,4 -DA:41,1 -DA:42,3 +DA:14,0 +DA:29,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:42,0 LF:11 -LH:11 +LH:0 end_of_record SF:lib/src/components/picker/t_picker_value.dart -DA:13,1 -DA:28,1 -DA:29,5 -DA:35,1 -DA:36,5 -DA:38,1 -DA:40,4 -LF:7 -LH:7 +DA:13,0 +DA:38,0 +DA:40,0 +LF:3 +LH:0 end_of_record SF:lib/src/components/calendar/t_calendar.dart DA:26,0 @@ -2477,36 +2493,36 @@ DA:25,0 DA:61,0 DA:64,0 DA:67,0 +DA:68,0 DA:71,0 -DA:74,0 -DA:75,0 -DA:76,0 +DA:72,0 +DA:73,0 DA:77,0 +DA:79,0 DA:80,0 DA:81,0 -DA:82,0 -DA:83,0 +DA:84,0 DA:85,0 DA:86,0 -DA:87,0 -DA:92,0 -DA:93,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:96,0 DA:97,0 DA:98,0 -DA:99,0 +DA:102,0 DA:103,0 DA:104,0 DA:105,0 -DA:106,0 +DA:110,0 DA:111,0 -DA:113,0 -DA:118,0 -DA:120,0 -DA:125,0 -DA:133,0 -DA:141,0 -DA:146,0 -DA:147,0 +DA:114,0 +DA:116,0 +DA:121,0 +DA:129,0 +DA:137,0 +DA:142,0 +DA:143,0 LF:38 LH:0 end_of_record @@ -3812,7 +3828,7 @@ LF:10 LH:0 end_of_record SF:lib/src/components/collapse/t_inset_divider.dart -DA:9,1 +DA:9,5 DA:11,0 DA:13,0 DA:15,0 @@ -4281,7 +4297,7 @@ LF:47 LH:0 end_of_record SF:lib/src/components/divider/t_divider.dart -DA:12,6 +DA:12,30 DA:26,0 DA:64,0 DA:67,0 @@ -4338,10 +4354,9 @@ DA:108,0 DA:109,0 DA:111,0 DA:112,0 +DA:113,0 +DA:116,0 DA:117,0 -DA:118,0 -DA:119,0 -DA:120,0 DA:121,0 DA:122,0 DA:123,0 @@ -4353,22 +4368,21 @@ DA:128,0 DA:129,0 DA:130,0 DA:131,0 -DA:136,0 -DA:138,0 -DA:142,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:139,0 +DA:140,0 DA:143,0 -DA:146,0 +DA:145,0 DA:148,0 DA:149,0 DA:150,0 -DA:154,0 -DA:155,0 -DA:156,0 -LF:35 +LF:33 LH:0 end_of_record SF:lib/src/components/icon/t_icons.dart -DA:8,1 +DA:8,5 DA:9,0 DA:22,0 LF:3 @@ -6286,11 +6300,11 @@ LF:518 LH:0 end_of_record SF:lib/src/components/image_viewer/t_image_viewer.dart -DA:12,0 +DA:11,0 +DA:40,0 DA:41,0 -DA:42,0 +DA:46,0 DA:47,0 -DA:48,0 LF:5 LH:0 end_of_record @@ -6756,7 +6770,7 @@ DA:94,0 DA:98,0 DA:102,0 DA:104,0 -DA:111,1 +DA:111,5 DA:120,0 DA:125,0 DA:129,0 @@ -7817,7 +7831,7 @@ LF:47 LH:0 end_of_record SF:lib/src/components/loading/t_loading.dart -DA:38,3 +DA:38,15 DA:49,0 DA:78,0 DA:80,0 @@ -8321,12 +8335,12 @@ LF:21 LH:0 end_of_record SF:lib/src/util/platform_util.dart -DA:6,1 -DA:7,1 -DA:10,1 -DA:11,1 -DA:14,1 -DA:15,1 +DA:6,0 +DA:7,0 +DA:10,3 +DA:11,3 +DA:14,0 +DA:15,0 DA:18,0 DA:19,0 DA:22,0 @@ -8337,409 +8351,23 @@ DA:30,0 DA:31,0 DA:34,0 LF:15 -LH:6 +LH:2 end_of_record SF:lib/src/components/picker/t_picker.dart -DA:57,2 -DA:229,1 -DA:230,1 -DA:250,6 -DA:252,1 -DA:254,1 -DA:255,1 -DA:258,1 -DA:260,1 -DA:261,4 -DA:263,4 -DA:265,1 -DA:266,1 -DA:270,1 -DA:272,1 -DA:273,1 -DA:277,1 -DA:278,2 -DA:279,1 -DA:283,1 -DA:284,2 -DA:285,2 -DA:286,2 -DA:287,2 -DA:288,2 -DA:290,2 -DA:291,2 -DA:292,1 -DA:293,1 -DA:294,2 -DA:295,1 -DA:296,1 -DA:300,1 -DA:301,1 -DA:303,2 -DA:304,5 -DA:306,2 -DA:308,7 -DA:309,1 -DA:313,1 -DA:317,1 -DA:318,2 -DA:319,1 -DA:323,2 -DA:324,1 -DA:327,1 -DA:328,2 -DA:331,5 -DA:332,1 -DA:337,1 -DA:339,1 -DA:340,1 -DA:342,1 -DA:343,1 -DA:344,1 -DA:350,1 -DA:351,1 -DA:352,2 -DA:353,1 -DA:354,2 -DA:355,1 -DA:356,2 -DA:358,1 -DA:360,1 -DA:362,1 -DA:363,5 -DA:364,1 -DA:365,1 -DA:366,1 -DA:367,1 -DA:368,1 -DA:369,1 -DA:370,2 -DA:375,1 -DA:376,2 -DA:377,1 -DA:378,1 -DA:379,4 -DA:380,2 -DA:392,1 -DA:393,1 -DA:395,1 -DA:396,2 -DA:397,1 -DA:398,1 -DA:399,1 -DA:401,1 -DA:402,4 -DA:403,4 -DA:404,1 -DA:405,2 -DA:407,1 -DA:408,2 -DA:410,1 -DA:412,1 -DA:413,4 -DA:414,5 -DA:415,1 -DA:416,2 -DA:425,1 -DA:426,2 -DA:427,2 -DA:429,1 -DA:430,2 -DA:431,1 -DA:432,2 -DA:434,1 -DA:446,1 -DA:454,1 -DA:455,1 -DA:456,2 -DA:459,1 -DA:460,1 -DA:465,1 -DA:467,2 -DA:468,2 -DA:469,0 -DA:471,1 -DA:474,1 -DA:475,1 -DA:476,2 -DA:483,1 -DA:484,2 -DA:485,1 -DA:489,1 -DA:490,1 -DA:492,1 -DA:493,1 -DA:494,1 -DA:495,1 -DA:496,1 -DA:497,1 -DA:498,1 -DA:500,2 -DA:501,2 -DA:504,2 -DA:506,2 -DA:507,1 -DA:508,1 -DA:509,2 -DA:510,2 -DA:511,2 -DA:514,1 -DA:515,2 -DA:516,2 -DA:517,2 -DA:526,1 -DA:529,1 -DA:533,2 -DA:534,1 -DA:537,1 -DA:538,2 -DA:539,2 -DA:543,1 -DA:545,2 -DA:550,2 -DA:554,1 -DA:559,1 -DA:561,3 -DA:562,1 -DA:565,2 -DA:568,1 -DA:569,5 -DA:570,0 -DA:574,2 -DA:575,1 -DA:579,1 -DA:581,1 -DA:584,2 -DA:585,1 -DA:586,2 -DA:587,1 -DA:589,1 -DA:590,0 -DA:591,0 -DA:596,1 -DA:597,2 -DA:601,1 -DA:602,1 -DA:603,3 -DA:604,1 -DA:605,1 -DA:606,2 -DA:610,1 -DA:611,2 -DA:618,1 -DA:619,2 -DA:620,1 -DA:621,4 -DA:624,0 -DA:625,0 -DA:629,0 -DA:632,1 -DA:633,2 -DA:634,3 -DA:635,3 -DA:638,1 -DA:642,2 -DA:646,2 -DA:648,1 -DA:651,1 -DA:658,1 -DA:659,4 -DA:662,5 -DA:663,3 -DA:665,5 -DA:666,5 -DA:667,5 -DA:668,5 -DA:677,1 -DA:686,2 -DA:687,1 -DA:688,3 -DA:691,2 -DA:692,3 -DA:693,2 -DA:694,4 -DA:702,3 -DA:713,1 -DA:714,1 -DA:715,1 -DA:718,2 -DA:720,1 -DA:721,1 -DA:730,1 -DA:731,1 -DA:732,1 -DA:734,4 -DA:735,2 -DA:736,1 -DA:739,6 -DA:741,2 -DA:742,0 -DA:743,0 -DA:747,1 -DA:748,2 -DA:751,1 -DA:754,1 -DA:755,4 -DA:762,1 -DA:763,2 -DA:767,2 -DA:768,1 -DA:771,2 -DA:773,4 -LF:245 -LH:236 -end_of_record -SF:lib/src/components/picker/t_picker_items.dart -DA:14,2 -DA:29,2 -DA:39,1 -DA:43,1 -DA:44,1 -DA:50,1 -DA:53,1 -DA:54,3 -DA:55,3 -DA:58,1 -DA:63,3 -DA:66,3 -DA:67,3 -DA:74,1 -DA:75,2 -DA:93,1 -DA:103,1 -DA:107,1 -DA:108,1 -DA:118,1 -DA:121,1 -DA:122,3 -DA:123,3 -DA:130,1 -DA:135,3 -DA:138,2 -DA:139,2 -DA:140,2 -DA:141,5 -DA:142,5 -DA:149,1 -DA:153,0 -DA:154,0 -DA:156,0 -DA:157,0 -DA:159,0 -DA:162,1 -DA:163,2 -DA:164,2 -DA:165,5 -LF:40 -LH:35 -end_of_record -SF:lib/src/components/picker/t_picker_keys.dart -DA:13,2 -DA:35,1 -DA:38,1 -DA:39,3 -DA:40,3 -DA:41,3 -DA:42,3 -DA:43,3 -DA:45,1 -DA:46,5 -DA:48,1 -DA:50,5 -LF:12 -LH:12 -end_of_record -SF:lib/src/components/picker/t_picker_load_event.dart -DA:8,2 -DA:29,1 -DA:30,1 -DA:31,2 -DA:32,2 -LF:5 -LH:5 -end_of_record -SF:lib/src/components/popover/t_popover.dart -DA:6,0 -DA:24,0 -DA:29,0 -LF:3 -LH:0 -end_of_record -SF:lib/src/components/popover/t_popover_widget.dart -DA:66,0 -DA:126,0 -DA:127,0 -DA:135,0 -DA:137,0 -DA:138,0 -DA:139,0 -DA:140,0 -DA:141,0 -DA:144,0 -DA:145,0 -DA:152,0 -DA:153,0 -DA:154,0 -DA:155,0 -DA:159,0 -DA:160,0 -DA:161,0 -DA:164,0 -DA:165,0 -DA:169,0 -DA:170,0 -DA:171,0 -DA:172,0 -DA:173,0 -DA:174,0 -DA:175,0 -DA:178,0 -DA:179,0 -DA:183,0 -DA:184,0 -DA:188,0 -DA:189,0 -DA:190,0 -DA:191,0 -DA:192,0 -DA:193,0 -DA:197,0 -DA:198,0 -DA:202,0 -DA:203,0 -DA:204,0 -DA:207,0 -DA:208,0 -DA:209,0 -DA:210,0 -DA:211,0 -DA:212,0 -DA:216,0 -DA:217,0 -DA:221,0 -DA:222,0 -DA:223,0 -DA:227,0 -DA:230,0 -DA:235,0 -DA:236,0 -DA:237,0 -DA:238,0 -DA:239,0 -DA:241,0 -DA:242,0 -DA:243,0 -DA:245,0 +DA:53,0 +DA:225,0 +DA:226,0 +DA:244,0 DA:246,0 -DA:247,0 +DA:248,0 DA:249,0 -DA:250,0 -DA:251,0 -DA:253,0 +DA:252,0 DA:254,0 DA:255,0 -DA:258,0 +DA:257,0 DA:259,0 -DA:265,0 +DA:260,0 +DA:264,0 DA:266,0 DA:267,0 DA:271,0 @@ -8751,7 +8379,6 @@ DA:279,0 DA:280,0 DA:281,0 DA:282,0 -DA:283,0 DA:284,0 DA:285,0 DA:286,0 @@ -8759,243 +8386,434 @@ DA:287,0 DA:288,0 DA:289,0 DA:290,0 -DA:291,0 -DA:293,0 DA:294,0 DA:295,0 -DA:296,0 DA:297,0 DA:298,0 DA:300,0 -DA:305,0 -DA:306,0 +DA:302,0 +DA:303,0 DA:307,0 -DA:308,0 -DA:309,0 -DA:310,0 DA:311,0 DA:312,0 DA:313,0 -DA:314,0 -DA:315,0 DA:317,0 DA:318,0 -DA:319,0 -DA:320,0 DA:321,0 DA:322,0 -DA:323,0 -DA:324,0 DA:325,0 DA:326,0 -DA:327,0 -DA:329,0 -DA:335,0 +DA:331,0 +DA:333,0 +DA:334,0 DA:336,0 DA:337,0 DA:338,0 -DA:340,0 -DA:342,0 -DA:343,0 DA:344,0 +DA:345,0 DA:346,0 DA:347,0 DA:348,0 +DA:349,0 DA:350,0 -DA:351,0 -DA:353,0 +DA:352,0 DA:354,0 -DA:355,0 +DA:356,0 DA:357,0 +DA:358,0 DA:359,0 +DA:360,0 DA:361,0 DA:362,0 +DA:363,0 DA:364,0 -DA:365,0 -DA:366,0 -DA:368,0 +DA:369,0 DA:370,0 +DA:371,0 DA:372,0 DA:373,0 -DA:375,0 -DA:376,0 -DA:377,0 -DA:380,0 -DA:382,0 -DA:384,0 +DA:374,0 +DA:386,0 +DA:387,0 +DA:388,0 DA:389,0 -DA:390,0 DA:391,0 +DA:392,0 DA:393,0 +DA:394,0 +DA:395,0 +DA:396,0 DA:397,0 DA:398,0 DA:399,0 -DA:400,0 DA:401,0 DA:402,0 +DA:407,0 DA:408,0 DA:410,0 DA:411,0 DA:412,0 DA:413,0 +DA:414,0 DA:416,0 DA:417,0 -DA:418,0 -DA:419,0 -DA:420,0 -DA:421,0 -DA:422,0 -DA:423,0 -DA:424,0 -DA:442,0 -DA:443,0 -DA:444,0 +DA:429,0 +DA:430,0 +DA:431,0 +DA:433,0 +DA:434,0 +DA:435,0 +DA:436,0 +DA:438,0 DA:445,0 DA:446,0 +DA:447,0 +DA:451,0 +DA:452,0 +DA:454,0 DA:455,0 DA:456,0 DA:457,0 DA:458,0 DA:459,0 DA:460,0 -DA:467,0 +DA:462,0 +DA:463,0 +DA:466,0 DA:468,0 DA:469,0 DA:470,0 +DA:471,0 +DA:472,0 DA:473,0 -DA:474,0 -DA:475,0 +DA:476,0 +DA:477,0 DA:478,0 DA:479,0 -DA:480,0 -DA:481,0 -DA:483,0 -DA:492,0 -DA:493,0 -DA:494,0 -DA:497,0 -DA:498,0 +DA:488,0 +DA:491,0 +DA:495,0 +DA:496,0 +DA:499,0 +DA:500,0 DA:501,0 -DA:502,0 DA:505,0 -DA:506,0 -DA:514,0 -DA:515,0 +DA:507,0 +DA:512,0 DA:516,0 -DA:517,0 -DA:518,0 -DA:519,0 -DA:520,0 +DA:521,0 +DA:523,0 +DA:524,0 DA:527,0 -DA:534,0 +DA:530,0 +DA:531,0 +DA:532,0 DA:536,0 DA:537,0 -DA:538,0 -DA:539,0 -DA:540,0 DA:541,0 -DA:542,0 -DA:545,0 -LF:231 +DA:543,0 +DA:546,0 +DA:547,0 +DA:548,0 +DA:549,0 +DA:551,0 +DA:552,0 +DA:553,0 +DA:558,0 +DA:559,0 +DA:563,0 +DA:564,0 +DA:565,0 +DA:566,0 +DA:567,0 +DA:568,0 +DA:572,0 +DA:573,0 +DA:580,0 +DA:581,0 +DA:582,0 +DA:583,0 +DA:586,0 +DA:587,0 +DA:591,0 +DA:594,0 +DA:595,0 +DA:596,0 +DA:597,0 +DA:600,0 +DA:604,0 +DA:608,0 +DA:610,0 +DA:613,0 +DA:620,0 +DA:621,0 +DA:624,0 +DA:625,0 +DA:627,0 +DA:628,0 +DA:629,0 +DA:630,0 +DA:639,0 +DA:648,0 +DA:649,0 +DA:650,0 +DA:653,0 +DA:654,0 +DA:655,0 +DA:656,0 +DA:664,0 +DA:675,0 +DA:676,0 +DA:677,0 +DA:680,0 +DA:682,0 +DA:683,0 +DA:692,0 +DA:693,0 +DA:694,0 +DA:696,0 +DA:697,0 +DA:698,0 +DA:701,0 +DA:703,0 +DA:704,0 +DA:705,0 +DA:709,0 +DA:710,0 +DA:713,0 +DA:716,0 +DA:717,0 +DA:724,0 +DA:725,0 +DA:729,0 +DA:730,0 +DA:733,0 +DA:735,0 +LF:235 LH:0 end_of_record -SF:lib/src/components/popup/t_popup_panel.dart -DA:11,0 -DA:21,0 -DA:70,0 -DA:72,0 -DA:73,0 -DA:77,0 -DA:78,0 -DA:80,0 -DA:88,0 -DA:90,0 -DA:94,0 -DA:95,0 -DA:99,0 -DA:100,0 +SF:lib/src/util/t_toolbar_pressable.dart +DA:10,3 +DA:41,3 +DA:42,3 +DA:48,2 +DA:49,8 +DA:52,4 +DA:55,6 +DA:58,3 +DA:60,3 +DA:61,6 +DA:62,3 +DA:63,3 +DA:64,3 +DA:67,6 +DA:68,6 +DA:69,1 +DA:70,2 +DA:74,6 +DA:75,1 +DA:76,2 +DA:81,12 +DA:83,3 +DA:85,4 +DA:86,4 +DA:87,0 +DA:88,6 +DA:89,3 +DA:90,6 +DA:91,3 +DA:92,3 +LF:30 +LH:29 +end_of_record +SF:lib/src/components/picker/t_picker_items.dart +DA:14,0 +DA:29,0 +DA:39,0 +DA:43,0 +DA:44,0 +DA:50,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:58,0 +DA:63,0 +DA:66,0 +DA:67,0 +DA:74,0 +DA:75,0 +DA:93,0 DA:103,0 -DA:104,0 -DA:105,0 -DA:106,0 DA:107,0 -DA:110,0 -DA:111,0 -DA:112,0 -DA:113,0 -DA:114,0 -DA:116,0 -DA:117,0 -DA:120,0 +DA:108,0 +DA:118,0 DA:121,0 -DA:124,0 -DA:125,0 +DA:122,0 +DA:123,0 +DA:130,0 +DA:135,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:149,0 +DA:153,0 +DA:154,0 +DA:156,0 +DA:157,0 +DA:159,0 +DA:162,0 +DA:163,0 +DA:164,0 +DA:165,0 +LF:40 +LH:0 +end_of_record +SF:lib/src/components/picker/t_picker_keys.dart +DA:13,5 +DA:35,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:45,0 +DA:46,0 +DA:48,0 +DA:50,0 +LF:12 +LH:1 +end_of_record +SF:lib/src/components/picker/t_picker_normalize.dart +DA:9,0 +DA:12,0 +DA:14,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:22,0 +DA:27,0 +DA:29,0 +DA:32,0 +DA:35,0 +DA:36,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:46,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:63,0 +DA:64,0 +LF:30 +LH:0 +end_of_record +SF:lib/src/components/picker/t_picker_load_event.dart +DA:8,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +LF:5 +LH:0 +end_of_record +SF:lib/src/components/popover/t_popover.dart +DA:6,0 +DA:24,0 +DA:29,0 +LF:3 +LH:0 +end_of_record +SF:lib/src/components/popover/t_popover_widget.dart +DA:66,0 DA:126,0 DA:127,0 -DA:131,0 +DA:135,0 +DA:137,0 +DA:138,0 +DA:139,0 DA:140,0 DA:141,0 DA:144,0 DA:145,0 -DA:146,0 -DA:149,0 -DA:150,0 +DA:152,0 +DA:153,0 DA:154,0 DA:155,0 -DA:156,0 -DA:157,0 -DA:158,0 +DA:159,0 +DA:160,0 DA:161,0 -DA:168,0 +DA:164,0 +DA:165,0 DA:169,0 +DA:170,0 +DA:171,0 DA:172,0 +DA:173,0 DA:174,0 DA:175,0 -DA:176,0 -DA:177,0 -DA:181,0 +DA:178,0 +DA:179,0 +DA:183,0 DA:184,0 -DA:185,0 +DA:188,0 DA:189,0 +DA:190,0 DA:191,0 DA:192,0 DA:193,0 -DA:194,0 DA:197,0 -DA:200,0 -DA:201,0 +DA:198,0 DA:202,0 +DA:203,0 +DA:204,0 +DA:207,0 +DA:208,0 DA:209,0 +DA:210,0 DA:211,0 -DA:213,0 -DA:214,0 -DA:218,0 -DA:219,0 -DA:220,0 +DA:212,0 +DA:216,0 +DA:217,0 DA:221,0 DA:222,0 DA:223,0 -DA:225,0 -DA:226,0 -DA:228,0 -DA:229,0 +DA:227,0 DA:230,0 -DA:232,0 -DA:233,0 -DA:234,0 DA:235,0 DA:236,0 -DA:244,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:241,0 +DA:242,0 +DA:243,0 DA:245,0 DA:246,0 DA:247,0 -DA:248,0 DA:249,0 DA:250,0 DA:251,0 -DA:256,0 -DA:257,0 +DA:253,0 +DA:254,0 +DA:255,0 DA:258,0 +DA:259,0 +DA:265,0 +DA:266,0 +DA:267,0 DA:271,0 DA:272,0 DA:273,0 @@ -9003,130 +8821,544 @@ DA:277,0 DA:278,0 DA:279,0 DA:280,0 +DA:281,0 +DA:282,0 DA:283,0 DA:284,0 DA:285,0 +DA:286,0 DA:287,0 DA:288,0 DA:289,0 DA:290,0 -DA:292,0 +DA:291,0 +DA:293,0 +DA:294,0 +DA:295,0 +DA:296,0 +DA:297,0 DA:298,0 -DA:334,0 +DA:300,0 +DA:305,0 +DA:306,0 +DA:307,0 +DA:308,0 +DA:309,0 +DA:310,0 +DA:311,0 +DA:312,0 +DA:313,0 +DA:314,0 +DA:315,0 +DA:317,0 +DA:318,0 +DA:319,0 +DA:320,0 +DA:321,0 +DA:322,0 +DA:323,0 +DA:324,0 +DA:325,0 +DA:326,0 +DA:327,0 +DA:329,0 DA:335,0 +DA:336,0 +DA:337,0 +DA:338,0 DA:340,0 DA:342,0 DA:343,0 -DA:345,0 +DA:344,0 DA:346,0 DA:347,0 DA:348,0 -DA:349,0 DA:350,0 +DA:351,0 +DA:353,0 +DA:354,0 +DA:355,0 DA:357,0 -DA:358,0 -DA:360,0 +DA:359,0 DA:361,0 -DA:363,0 +DA:362,0 +DA:364,0 +DA:365,0 DA:366,0 DA:368,0 -DA:369,0 -DA:371,0 +DA:370,0 DA:372,0 -DA:374,0 -DA:381,0 +DA:373,0 +DA:375,0 +DA:376,0 +DA:377,0 +DA:380,0 DA:382,0 -DA:383,0 +DA:384,0 +DA:389,0 DA:390,0 -DA:392,0 -DA:394,0 -DA:395,0 -DA:396,0 +DA:391,0 +DA:393,0 DA:397,0 DA:398,0 +DA:399,0 +DA:400,0 +DA:401,0 DA:402,0 -DA:404,0 -DA:409,0 +DA:408,0 +DA:410,0 +DA:411,0 +DA:412,0 +DA:413,0 +DA:416,0 +DA:417,0 +DA:418,0 +DA:419,0 +DA:420,0 +DA:421,0 +DA:422,0 +DA:423,0 +DA:424,0 +DA:442,0 +DA:443,0 +DA:444,0 +DA:445,0 +DA:446,0 +DA:455,0 +DA:456,0 DA:457,0 DA:458,0 -DA:463,0 -DA:465,0 -DA:466,0 +DA:459,0 +DA:460,0 DA:467,0 +DA:468,0 +DA:469,0 DA:470,0 -DA:472,0 DA:473,0 DA:474,0 -DA:476,0 -DA:477,0 +DA:475,0 +DA:478,0 +DA:479,0 DA:480,0 DA:481,0 -DA:482,0 DA:483,0 -DA:485,0 -DA:486,0 -DA:487,0 -DA:488,0 -DA:495,0 -DA:496,0 +DA:492,0 +DA:493,0 +DA:494,0 +DA:497,0 DA:498,0 -DA:499,0 -DA:507,0 -DA:513,0 +DA:501,0 +DA:502,0 +DA:505,0 +DA:506,0 +DA:514,0 DA:515,0 DA:516,0 +DA:517,0 +DA:518,0 +DA:519,0 DA:520,0 -DA:524,0 -DA:525,0 -DA:526,0 DA:527,0 -DA:528,0 -DA:529,0 -DA:530,0 +DA:534,0 DA:536,0 +DA:537,0 DA:538,0 +DA:539,0 +DA:540,0 DA:541,0 DA:542,0 -DA:543,0 -DA:544,0 DA:545,0 -DA:549,0 -DA:551,0 -DA:556,0 -DA:588,0 -DA:590,0 -DA:591,0 -DA:593,0 -DA:595,0 -DA:597,0 -DA:598,0 -DA:599,0 -DA:600,0 -DA:602,0 -DA:604,0 -DA:605,0 -DA:607,0 -DA:608,0 -DA:610,0 -DA:616,0 -DA:617,0 -DA:618,0 -DA:619,0 -DA:620,0 -DA:622,0 -DA:623,0 -DA:624,0 -DA:625,0 -DA:626,0 -DA:627,0 -DA:628,0 -DA:629,0 -DA:631,0 -DA:632,0 -DA:634,0 -LF:224 +LF:231 LH:0 end_of_record +SF:lib/src/components/popup/_popup_center_close.dart +DA:11,2 +DA:16,4 +DA:17,2 +DA:18,2 +DA:19,4 +DA:20,2 +DA:22,2 +DA:28,4 +DA:33,2 +DA:44,2 +DA:46,2 +DA:47,4 +DA:48,2 +DA:49,4 +DA:50,4 +DA:51,2 +DA:55,2 +DA:56,6 +DA:57,4 +DA:60,2 +DA:62,2 +DA:66,2 +DA:69,2 +LF:23 +LH:23 +end_of_record +SF:lib/src/components/popup/t_popup_types.dart +DA:26,5 +DA:28,1 +DA:37,3 +DA:68,1 +DA:73,4 +DA:74,4 +DA:86,1 +DA:91,3 +DA:92,3 +LF:9 +LH:9 +end_of_record +SF:lib/src/components/popup/_popup_header.dart +DA:15,3 +DA:26,3 +DA:28,15 +DA:32,6 +DA:33,6 +DA:35,2 +DA:39,6 +DA:40,3 +DA:42,3 +DA:43,3 +DA:44,3 +DA:49,3 +DA:54,2 +DA:56,2 +DA:57,4 +DA:66,2 +DA:67,2 +DA:68,2 +DA:69,2 +DA:70,4 +DA:71,2 +DA:73,4 +DA:74,2 +DA:76,4 +DA:77,4 +DA:81,3 +DA:82,6 +DA:83,2 +DA:85,12 +DA:86,2 +DA:87,4 +DA:88,4 +DA:89,4 +DA:98,2 +DA:99,4 +DA:100,3 +DA:102,6 +DA:103,1 +DA:104,4 +DA:105,1 +DA:106,1 +DA:109,2 +DA:112,2 +DA:113,4 +DA:114,3 +DA:116,6 +DA:117,1 +DA:118,4 +DA:119,1 +DA:120,1 +DA:124,2 +DA:129,3 +DA:137,3 +DA:139,3 +DA:140,6 +DA:141,3 +DA:142,6 +DA:143,3 +DA:144,3 +DA:150,3 +DA:151,3 +DA:152,6 +DA:153,3 +DA:154,6 +DA:155,3 +DA:157,6 +DA:159,3 +DA:160,2 +DA:161,5 +DA:162,4 +DA:163,4 +DA:166,3 +DA:171,2 +DA:172,6 +DA:173,6 +DA:174,3 +DA:175,6 +DA:176,3 +DA:178,6 +DA:180,3 +DA:181,1 +DA:182,3 +DA:183,2 +DA:184,2 +DA:187,3 +DA:192,2 +DA:197,3 +DA:198,6 +DA:199,3 +DA:201,9 +DA:202,3 +DA:203,12 +DA:204,3 +DA:205,3 +DA:208,4 +DA:211,3 +DA:212,6 +DA:213,3 +DA:215,9 +DA:216,3 +DA:217,12 +DA:218,3 +DA:219,3 +DA:223,4 +DA:227,3 +DA:228,3 +DA:229,1 +DA:232,6 +DA:235,3 +DA:236,3 +DA:237,1 +DA:240,6 +LF:112 +LH:112 +end_of_record +SF:lib/src/components/popup/_popup_layout.dart +DA:7,4 +DA:26,4 +DA:27,8 +DA:30,4 +DA:33,4 +DA:34,4 +DA:35,4 +DA:36,4 +DA:37,3 +DA:38,3 +DA:39,3 +DA:40,3 +DA:41,3 +DA:44,4 +DA:45,4 +DA:46,8 +DA:47,2 +DA:48,2 +DA:49,2 +DA:50,2 +DA:56,3 +DA:57,3 +DA:58,3 +DA:59,3 +DA:64,1 +DA:65,2 +DA:66,1 +DA:67,1 +DA:68,1 +DA:71,3 +DA:72,3 +DA:73,3 +DA:74,3 +DA:75,3 +DA:76,3 +DA:79,3 +DA:80,3 +DA:81,3 +DA:82,3 +DA:83,3 +DA:84,3 +DA:87,3 +DA:88,3 +DA:89,3 +DA:90,3 +DA:91,6 +DA:92,6 +DA:100,4 +DA:101,4 +DA:102,4 +DA:104,6 +DA:105,12 +DA:110,2 +DA:111,2 +DA:112,2 +DA:114,2 +DA:116,2 +DA:118,2 +DA:120,1 +DA:125,4 +DA:126,4 +DA:127,4 +DA:128,6 +DA:129,4 +DA:130,8 +DA:131,2 +DA:132,4 +DA:133,2 +DA:134,4 +DA:135,1 +LF:70 +LH:70 +end_of_record +SF:lib/src/components/popup/_popup_route.dart +DA:10,3 +DA:13,3 +DA:14,3 +DA:16,3 +DA:17,3 +DA:18,3 +DA:32,3 +DA:33,6 +DA:36,6 +DA:37,6 +DA:38,3 +DA:39,3 +DA:44,3 +DA:45,6 +DA:47,3 +DA:48,6 +DA:50,3 +DA:53,3 +DA:55,9 +DA:57,3 +DA:63,3 +DA:66,3 +DA:67,6 +DA:70,3 +DA:71,3 +DA:74,3 +DA:75,8 +DA:76,8 +DA:79,1 +DA:88,3 +DA:95,3 +DA:101,3 +DA:102,6 +DA:103,3 +DA:104,6 +DA:106,6 +DA:107,6 +DA:108,3 +DA:109,6 +DA:110,6 +DA:111,6 +DA:113,9 +DA:114,4 +DA:117,3 +DA:118,3 +DA:119,3 +DA:120,3 +DA:124,9 +DA:125,2 +DA:131,3 +DA:132,6 +DA:137,6 +DA:139,3 +DA:141,3 +DA:143,3 +DA:144,9 +DA:145,12 +DA:146,3 +DA:147,3 +DA:152,3 +DA:153,3 +DA:155,3 +DA:156,3 +DA:157,6 +DA:158,9 +DA:162,6 +DA:163,3 +DA:164,3 +DA:169,6 +DA:170,3 +DA:175,3 +DA:176,3 +DA:177,1 +DA:182,2 +DA:183,5 +DA:184,4 +DA:185,4 +DA:189,3 +DA:190,6 +DA:191,3 +DA:192,7 +DA:194,6 +DA:195,3 +DA:196,7 +DA:200,3 +DA:201,3 +DA:204,3 +DA:208,3 +DA:209,6 +DA:210,6 +DA:211,3 +DA:215,3 +DA:217,7 +DA:218,8 +DA:219,3 +DA:220,6 +DA:224,3 +DA:226,3 +DA:227,3 +DA:230,3 +DA:232,3 +DA:233,9 +DA:235,3 +LF:103 +LH:103 +end_of_record +SF:lib/src/components/popup/_popup_shell.dart +DA:13,3 +DA:22,3 +DA:24,3 +DA:25,9 +DA:27,9 +DA:28,9 +DA:30,6 +DA:32,9 +DA:33,4 +DA:34,2 +DA:35,2 +DA:37,2 +DA:42,2 +DA:43,2 +DA:45,2 +DA:48,2 +DA:49,2 +DA:50,4 +DA:51,4 +DA:52,2 +DA:53,2 +DA:55,2 +DA:64,9 +DA:65,9 +DA:66,6 +DA:68,3 +DA:69,3 +DA:74,9 +DA:75,3 +DA:79,3 +DA:80,3 +DA:81,3 +DA:82,3 +DA:84,5 +DA:88,1 +DA:91,2 +DA:99,3 +DA:101,3 +DA:102,4 +DA:103,3 +DA:104,6 +DA:105,2 +DA:106,2 +DA:107,2 +DA:108,2 +DA:109,2 +DA:110,2 +LF:47 +LH:47 +end_of_record SF:lib/src/components/progress/t_progress.dart DA:13,0 DA:17,0 @@ -10493,7 +10725,7 @@ LF:90 LH:0 end_of_record SF:lib/src/components/skeleton/t_skeleton_rowcol.dart -DA:7,1 +DA:7,5 DA:15,0 DA:16,0 DA:21,0 @@ -10507,11 +10739,11 @@ DA:41,0 DA:42,0 DA:43,0 DA:44,0 -DA:50,1 -DA:56,1 -DA:60,1 -DA:64,1 -DA:68,1 +DA:50,5 +DA:56,5 +DA:60,5 +DA:64,5 +DA:68,5 DA:79,0 DA:80,0 DA:83,0 @@ -10522,10 +10754,10 @@ DA:92,0 DA:95,0 DA:96,0 DA:101,0 -DA:110,1 -DA:119,1 -DA:128,1 -DA:137,1 +DA:110,5 +DA:119,5 +DA:128,5 +DA:137,5 DA:160,0 LF:34 LH:10 @@ -10884,7 +11116,7 @@ DA:523,0 DA:524,0 DA:525,0 DA:526,0 -DA:532,1 +DA:532,5 DA:534,0 DA:539,0 DA:561,0 @@ -12153,17 +12385,17 @@ LF:40 LH:0 end_of_record SF:lib/src/theme/basic.dart -DA:9,1 -DA:10,2 -DA:11,3 -DA:14,1 -DA:15,4 -DA:17,1 -DA:18,1 -DA:19,2 -DA:28,1 -DA:30,1 -DA:31,3 +DA:9,3 +DA:10,6 +DA:11,9 +DA:14,3 +DA:15,12 +DA:17,3 +DA:18,3 +DA:19,6 +DA:28,3 +DA:30,3 +DA:31,9 DA:36,0 DA:38,0 DA:39,0 @@ -12201,7 +12433,7 @@ DA:50,0 DA:52,0 DA:53,0 DA:54,0 -DA:88,1 +DA:88,5 DA:99,0 DA:101,0 DA:102,0 @@ -12242,7 +12474,7 @@ DA:158,0 DA:163,0 DA:164,0 DA:166,0 -DA:199,1 +DA:199,5 DA:211,0 DA:213,0 DA:214,0 @@ -12256,7 +12488,7 @@ DA:223,0 DA:225,0 DA:226,0 DA:227,0 -DA:251,1 +DA:251,5 DA:259,0 DA:261,0 DA:262,0 @@ -15317,19 +15549,19 @@ LF:176 LH:0 end_of_record SF:lib/src/theme/resource_delegate.dart -DA:20,0 -DA:21,0 +DA:20,3 +DA:21,3 DA:22,0 -DA:24,0 -DA:26,0 +DA:24,3 +DA:26,6 DA:31,0 DA:32,0 -DA:38,0 -DA:39,0 +DA:38,3 +DA:39,3 DA:44,0 -DA:47,0 -DA:48,0 -DA:49,0 +DA:47,3 +DA:48,3 +DA:49,3 DA:212,0 DA:215,0 DA:218,0 @@ -15382,7 +15614,7 @@ DA:356,0 DA:359,0 DA:362,0 LF:64 -LH:0 +LH:9 end_of_record SF:lib/src/theme/t_shadows.dart DA:7,0 @@ -15404,17 +15636,17 @@ LF:8 LH:0 end_of_record SF:lib/src/util/string_util.dart -DA:5,1 -DA:7,2 -DA:9,2 +DA:5,3 +DA:7,6 +DA:9,6 DA:14,0 -DA:17,2 -DA:18,1 -DA:20,1 -DA:23,2 -DA:24,1 -DA:27,1 -DA:30,2 +DA:17,6 +DA:18,3 +DA:20,3 +DA:23,6 +DA:24,3 +DA:27,3 +DA:30,6 LF:11 LH:10 end_of_record diff --git a/tdesign-component/demo_tool/all_build.sh b/tdesign-component/demo_tool/all_build.sh index 222124d19..f054eeb88 100644 --- a/tdesign-component/demo_tool/all_build.sh +++ b/tdesign-component/demo_tool/all_build.sh @@ -129,7 +129,7 @@ dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/compo # popover dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/popover" --name TPopover,TPopoverWidget --folder-name popover --output "$PARENT_DIR/example/assets/api/" --only-api --get-comments # popup -dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/popup" --name TSlidePopupRoute,TPopupBottomDisplayPanel,TPopupBottomConfirmPanel,TPopupCenterPanel --folder-name popup --output "$PARENT_DIR/example/assets/api/" --only-api --get-comments +dart run tdesign_flutter_tools:main generate --folder "$PARENT_DIR/lib/src/components/popup" --name TPopup,TPopupOptions,TPopupHandle,TPopupPlacement,TPopupTrigger --folder-name popup --output "$PARENT_DIR/example/assets/api/" --only-api --get-comments # refresh dart run tdesign_flutter_tools:main generate --file "$PARENT_DIR/lib/src/components/refresh/t_refresh_header.dart" --name TRefreshHeader --folder-name pull-down-refresh --output "$PARENT_DIR/example/assets/api/" --only-api --get-comments # swipecell diff --git a/tdesign-component/example/assets/api/action-sheet_api.md b/tdesign-component/example/assets/api/action-sheet_api.md index a08aa51e4..64bc4b9dd 100644 --- a/tdesign-component/example/assets/api/action-sheet_api.md +++ b/tdesign-component/example/assets/api/action-sheet_api.md @@ -9,14 +9,12 @@ | badge | TBadge? | - | 角标 | | description | String? | - | 描述信息 | | disabled | bool | false | 是否禁用 | -| group | String? | - | 分组,用于带描述多行滚动宫格 | +| group | String? | - | 分组,用于带描述多行滚动宫格 当[TActionSheet.theme]等于[TActionSheetTheme.group]时有效 有效时,如果该值未配置整个[TActionSheetItem]会被忽略,即不会展示 | | icon | Widget? | - | 图标 | | iconSize | double? | - | 图标大小 | | label | String | - | 标题 | | textStyle | TextStyle? | - | 标题样式 | -``` -``` ### TActionSheet #### 简介 @@ -25,23 +23,23 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | +| context | BuildContext | - | 上下文 | | align | TActionSheetAlign | TActionSheetAlign.center | 对齐方式 | | cancelText | String? | - | 取消按钮的文本 | | closeOnOverlayClick | bool | true | 点击蒙层时是否关闭 | -| context | BuildContext | context | 上下文 | -| count | int | 8 | 每页显示的项目数 | -| description | String? | - | 描述文本 | -| itemHeight | double | 96.0 | 项目的行高 | -| itemMinWidth | double | 80.0 | 项目的最小宽度 | +| count | int | 8 | 每页显示的项目数 当[theme]等于[TActionSheetTheme.grid]且[showPagination]为true时有效 | +| description | String? | - | 描述文本 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.list]时有效 | +| itemHeight | double | 96.0 | 项目的行高 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.group]时有效 | +| itemMinWidth | double | 80.0 | 项目的最小宽度 当[theme]等于[TActionSheetTheme.grid]且[scrollable]为true时有效 或当[theme]等于[TActionSheetTheme.group]时有效 | | items | List | - | ActionSheet的项目列表 | | onCancel | VoidCallback? | - | 取消按钮的回调函数 | | onClose | VoidCallback? | - | 关闭时的回调函数 | | onSelected | TActionSheetItemCallback? | - | 选择项目时的回调函数 | -| rows | int | 2 | 显示的行数 | -| scrollable | bool | false | 是否可以横向滚动 | +| rows | int | 2 | 显示的行数 当[theme]等于[TActionSheetTheme.grid]时有效 | +| scrollable | bool | false | 是否可以横向滚动 当[theme]等于[TActionSheetTheme.grid]且[showPagination]为false时有效 | | showCancel | bool | true | 是否显示取消按钮 | | showOverlay | bool | true | 是否显示遮罩层 | -| showPagination | bool | false | 是否显示分页 | +| showPagination | bool | false | 是否显示分页 当[theme]等于[TActionSheetTheme.grid]时有效 | | theme | TActionSheetTheme | TActionSheetTheme.list | 主题样式 | | useSafeArea | bool | true | 使用安全区域 | | visible | bool | false | 是否立即显示 | @@ -49,8 +47,103 @@ #### 静态方法 -| 名称 | 返回类型 | 参数 | 说明 | +##### TActionSheet.showGridActionSheet + +显示宫格类型面板 + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| showGridActionSheet | | required BuildContext context, required List items, TActionSheetAlign align, String? cancelText, bool showCancel, TActionSheetItemCallback? onSelected, bool showOverlay, bool closeOnOverlayClick, int count, int rows, double itemHeight, double itemMinWidth, bool scrollable, bool showPagination, VoidCallback? onCancel, String? description, VoidCallback? onClose, bool useSafeArea, | 显示宫格类型面板 | -| showGroupActionSheet | | required BuildContext context, required List items, TActionSheetAlign align, String? cancelText, bool showCancel, TActionSheetItemCallback? onSelected, bool showOverlay, bool closeOnOverlayClick, double itemHeight, double itemMinWidth, VoidCallback? onCancel, VoidCallback? onClose, bool useSafeArea, | 显示分组类型面板 | -| showListActionSheet | | required BuildContext context, required List items, TActionSheetAlign align, String? cancelText, bool showCancel, VoidCallback? onCancel, TActionSheetItemCallback? onSelected, bool showOverlay, bool closeOnOverlayClick, VoidCallback? onClose, bool useSafeArea, | 显示列表类型面板 | +| context | BuildContext | - | 上下文 | +| items | List | - | ActionSheet的项目列表 | +| align | TActionSheetAlign | TActionSheetAlign.center | 对齐方式 | +| cancelText | String? | - | 取消按钮的文本 | +| showCancel | bool | true | 是否显示取消按钮 | +| onSelected | TActionSheetItemCallback? | - | 选择项目时的回调函数 | +| showOverlay | bool | true | 是否显示遮罩层 | +| closeOnOverlayClick | bool | true | 点击蒙层时是否关闭 | +| count | int | 8 | 每页显示的项目数 当[theme]等于[TActionSheetTheme.grid]且[showPagination]为true时有效 | +| rows | int | 2 | 显示的行数 当[theme]等于[TActionSheetTheme.grid]时有效 | +| itemHeight | double | 96.0 | 项目的行高 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.group]时有效 | +| itemMinWidth | double | 80.0 | 项目的最小宽度 当[theme]等于[TActionSheetTheme.grid]且[scrollable]为true时有效 或当[theme]等于[TActionSheetTheme.group]时有效 | +| scrollable | bool | false | 是否可以横向滚动 当[theme]等于[TActionSheetTheme.grid]且[showPagination]为false时有效 | +| showPagination | bool | false | 是否显示分页 当[theme]等于[TActionSheetTheme.grid]时有效 | +| onCancel | VoidCallback? | - | 取消按钮的回调函数 | +| description | String? | - | 描述文本 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.list]时有效 | +| onClose | VoidCallback? | - | 关闭时的回调函数 | +| useSafeArea | bool | true | 使用安全区域 | + + +##### TActionSheet.showGroupActionSheet + +显示分组类型面板 + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | 上下文 | +| items | List | - | ActionSheet的项目列表 | +| align | TActionSheetAlign | TActionSheetAlign.left | 对齐方式 | +| cancelText | String? | - | 取消按钮的文本 | +| showCancel | bool | true | 是否显示取消按钮 | +| onSelected | TActionSheetItemCallback? | - | 选择项目时的回调函数 | +| showOverlay | bool | true | 是否显示遮罩层 | +| closeOnOverlayClick | bool | true | 点击蒙层时是否关闭 | +| itemHeight | double | 96.0 | 项目的行高 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.group]时有效 | +| itemMinWidth | double | 80.0 | 项目的最小宽度 当[theme]等于[TActionSheetTheme.grid]且[scrollable]为true时有效 或当[theme]等于[TActionSheetTheme.group]时有效 | +| onCancel | VoidCallback? | - | 取消按钮的回调函数 | +| onClose | VoidCallback? | - | 关闭时的回调函数 | +| useSafeArea | bool | true | 使用安全区域 | + + +##### TActionSheet.showListActionSheet + +显示列表类型面板 + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | 上下文 | +| items | List | - | ActionSheet的项目列表 | +| align | TActionSheetAlign | TActionSheetAlign.center | 对齐方式 | +| cancelText | String? | - | 取消按钮的文本 | +| showCancel | bool | true | 是否显示取消按钮 | +| onCancel | VoidCallback? | - | 取消按钮的回调函数 | +| onSelected | TActionSheetItemCallback? | - | 选择项目时的回调函数 | +| showOverlay | bool | true | 是否显示遮罩层 | +| closeOnOverlayClick | bool | true | 点击蒙层时是否关闭 | +| onClose | VoidCallback? | - | 关闭时的回调函数 | +| useSafeArea | bool | true | 使用安全区域 | + + +### TActionSheetTheme +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| list | - | +| grid | - | +| group | - | + + +### TActionSheetAlign +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| center | - | +| left | - | +| right | - | + + +### TActionSheetItemCallback +#### 类型定义 + +```dart +typedef TActionSheetItemCallback = void Function(TActionSheetItem item, int index); +``` diff --git a/tdesign-component/example/assets/api/avatar_api.md b/tdesign-component/example/assets/api/avatar_api.md index 9f1dd77b2..ddc9cf598 100644 --- a/tdesign-component/example/assets/api/avatar_api.md +++ b/tdesign-component/example/assets/api/avatar_api.md @@ -15,10 +15,44 @@ | displayText | String? | - | 纯展示类型末尾文字 | | fit | BoxFit? | - | 自定义图片对齐方式 | | icon | IconData? | - | 自定义图标 | -| key | | - | | -| onTap | Function()? | - | 操作点击事件 | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| onTap | Function()? | - | 操作点击事件 | | radius | double? | - | 自定义圆角 | | shape | TAvatarShape | TAvatarShape.circle | 头像形状 | | size | TAvatarSize | TAvatarSize.medium | 头像尺寸 | | text | String? | - | 自定义文字 | | type | TAvatarType | TAvatarType.normal | 头像类型 | + + +### TAvatarSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | - | +| medium | - | +| small | - | + + +### TAvatarType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| icon | - | +| normal | - | +| customText | - | +| display | - | +| operation | - | + + +### TAvatarShape +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| circle | - | +| square | - | diff --git a/tdesign-component/example/assets/api/back-top_api.md b/tdesign-component/example/assets/api/back-top_api.md index 0eaf4fdd6..8d2feb808 100644 --- a/tdesign-component/example/assets/api/back-top_api.md +++ b/tdesign-component/example/assets/api/back-top_api.md @@ -5,8 +5,28 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | controller | ScrollController? | - | 页面滚动的控制器 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onClick | VoidCallback? | - | 按钮点击事件 | | showText | bool | false | 是否展示文字 | | style | TBackTopStyle | TBackTopStyle.circle | 样式,圆形和半圆 | | theme | TBackTopTheme | TBackTopTheme.light | 主题 | + + +### TBackTopTheme +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| light | - | +| dark | - | + + +### TBackTopStyle +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| circle | - | +| halfCircle | - | diff --git a/tdesign-component/example/assets/api/badge_api.md b/tdesign-component/example/assets/api/badge_api.md index 620ac2afa..e91b2fb3a 100644 --- a/tdesign-component/example/assets/api/badge_api.md +++ b/tdesign-component/example/assets/api/badge_api.md @@ -4,16 +4,49 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | +| type | TBadgeType | - | 红点样式 | | border | TBadgeBorder | TBadgeBorder.large | 红点圆角大小 | | color | Color? | - | 红点颜色 | | count | String? | - | 红点数量 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | maxCount | String? | '99' | 最大红点数量 | | message | String? | - | 消息内容 | | padding | EdgeInsetsGeometry? | - | 角标自定义padding | | showZero | bool | true | 值为0是否显示 | | size | TBadgeSize | TBadgeSize.small | 红点尺寸 | | textColor | Color? | - | 文字颜色 | -| type | TBadgeType | type | 红点样式 | | widthLarge | double | 32 | 角标大三角形宽 | | widthSmall | double | 12 | 角标小三角形宽 | + + +### TBadgeType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| redPoint | 红点样式 | +| message | 消息样式 | +| bubble | 气泡样式 | +| square | 方形样式 | +| subscript | 角标样式 | + + +### TBadgeBorder +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | 大圆角 8px | +| small | 小圆角 2px | + + +### TBadgeSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | 宽 20px | +| small | 宽 16px | diff --git a/tdesign-component/example/assets/api/button_api.md b/tdesign-component/example/assets/api/button_api.md index be9eb3640..c036c83cb 100644 --- a/tdesign-component/example/assets/api/button_api.md +++ b/tdesign-component/example/assets/api/button_api.md @@ -16,7 +16,7 @@ | iconTextSpacing | double? | - | 自定义图标与文本之间距离 | | iconWidget | Widget? | - | 自定义图标 icon 控件 | | isBlock | bool | false | 是否为通栏按钮 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | margin | EdgeInsetsGeometry? | - | 自定义 margin | | onLongPress | TButtonEvent? | - | 长按事件 | | onTap | TButtonEvent? | - | 点击事件 | @@ -30,8 +30,6 @@ | type | TButtonType | TButtonType.fill | 类型:填充,描边,文字 | | width | double? | - | 自定义宽度 | -``` -``` ### TButtonStyle #### 默认构造方法 @@ -48,9 +46,123 @@ #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TButtonStyle.generateFillStyleByTheme | 生成不同主题的填充按钮样式 | -| TButtonStyle.generateGhostStyleByTheme | 生成不同主题的幽灵按钮样式 | -| TButtonStyle.generateOutlineStyleByTheme | 生成不同主题的描边按钮样式 | -| TButtonStyle.generateTextStyleByTheme | 生成不同主题的文本按钮样式 | +##### TButtonStyle.generateFillStyleByTheme + +生成不同主题的填充按钮样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | - | +| theme | TButtonTheme? | - | - | +| status | TButtonStatus | - | - | + + +##### TButtonStyle.generateGhostStyleByTheme + +生成不同主题的幽灵按钮样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | - | +| theme | TButtonTheme? | - | - | +| status | TButtonStatus | - | - | + + +##### TButtonStyle.generateOutlineStyleByTheme + +生成不同主题的描边按钮样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | - | +| theme | TButtonTheme? | - | - | +| status | TButtonStatus | - | - | + + +##### TButtonStyle.generateTextStyleByTheme + +生成不同主题的文本按钮样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | - | +| theme | TButtonTheme? | - | - | +| status | TButtonStatus | - | - | + + +### TButtonSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | - | +| medium | - | +| small | - | +| extraSmall | - | + + +### TButtonType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| fill | - | +| outline | - | +| text | - | +| ghost | - | + + +### TButtonShape +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| rectangle | - | +| round | - | +| square | - | +| circle | - | +| filled | - | + + +### TButtonTheme +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| defaultTheme | - | +| primary | - | +| danger | - | +| light | - | + + +### TButtonStatus +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| defaultState | - | +| active | - | +| disable | - | + + +### TButtonIconPosition +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| left | - | +| right | - | + + +### TButtonEvent +#### 类型定义 + +```dart +typedef TButtonEvent = void Function(); +``` diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md index eeb491358..e23fa0c7f 100644 --- a/tdesign-component/example/assets/api/calendar_api.md +++ b/tdesign-component/example/assets/api/calendar_api.md @@ -32,7 +32,7 @@ | 名称 | 返回类型 | 参数 | 说明 | | --- | --- | --- | --- | -| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, int anchorRevision, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick, bool autoClose, bool draggable, TCalendarCellBuilder? cellBuilder, TCalendarSubtitleBuilder? subtitleBuilder, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`, 或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | +| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, int anchorRevision, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick, bool autoClose, bool draggable, TCalendarCellBuilder? cellBuilder, TCalendarSubtitleBuilder? subtitleBuilder, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`, 或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopup.show] / [TPopupOptions.bottom] 自行组装。 | ``` ``` diff --git a/tdesign-component/example/assets/api/cascader_api.md b/tdesign-component/example/assets/api/cascader_api.md index 7906e3559..bfd99ac6a 100644 --- a/tdesign-component/example/assets/api/cascader_api.md +++ b/tdesign-component/example/assets/api/cascader_api.md @@ -12,7 +12,7 @@ | initialData | String? | - | 初始化数据 | | initialIndexes | List? | - | 若为null表示全部从零开始 | | isLetterSort | bool | false | 是否开启字母排序 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onChange | MultiCascaderCallback | - | 值发生变更时触发 | | onClose | Function? | - | 选择器关闭按钮回调 | | subTitles | List? | - | 每级展示的次标题 | @@ -20,3 +20,11 @@ | title | String? | - | 选择器标题 | | titleStyle | TextStyle? | - | 标题样式 | | topRadius | double? | - | 顶部圆角 | + + +### MultiCascaderCallback +#### 类型定义 + +```dart +typedef MultiCascaderCallback = void Function(List selected); +``` diff --git a/tdesign-component/example/assets/api/cell_api.md b/tdesign-component/example/assets/api/cell_api.md index b66815628..c7563c928 100644 --- a/tdesign-component/example/assets/api/cell_api.md +++ b/tdesign-component/example/assets/api/cell_api.md @@ -18,7 +18,7 @@ | imageCircle | double? | 50 | 主图圆角,默认50(圆形) | | imageSize | double? | - | 主图尺寸 | | imageWidget | Widget? | - | 主图组件 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftIcon | IconData? | - | 左侧图标,出现在单元格标题的左侧 | | leftIconWidget | Widget? | - | 左侧图标组件 | | note | String? | - | 和标题同行的说明文字 | @@ -35,8 +35,6 @@ | title | String? | - | 标题 | | titleWidget | Widget? | - | 标题组件 | -``` -``` ### TCellGroup #### 简介 @@ -49,15 +47,13 @@ | builder | CellBuilder? | - | cell构建器,可自定义cell父组件,如Dismissible | | cells | List | - | 单元格列表 | | isShowLastBordered | bool? | false | 是否显示最后一个cell的下边框 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | scrollable | bool? | false | 可滚动 | | style | TCellStyle? | - | 自定义样式 | | theme | TCellGroupTheme? | TCellGroupTheme.defaultTheme | 单元格组风格。可选项:default/card | | title | String? | - | 单元格组标题 | | titleWidget | Widget? | - | 单元格组标题组件 | -``` -``` ### TCellStyle #### 简介 @@ -88,6 +84,47 @@ #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TCellStyle.cellStyle | 生成单元格默认样式 | +##### TCellStyle.cellStyle + +生成单元格默认样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | 传递context,会生成默认样式 | + + +### TCellAlign +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| top | - | +| middle | - | +| bottom | - | + + +### TCellGroupTheme +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| defaultTheme | - | +| cardTheme | - | + + +### TCellClick +#### 类型定义 + +```dart +typedef TCellClick = void Function(TCell cell); +``` + + +### CellBuilder +#### 类型定义 + +```dart +typedef CellBuilder = Widget Function(BuildContext context, TCell cell, int index); +``` diff --git a/tdesign-component/example/assets/api/checkbox_api.md b/tdesign-component/example/assets/api/checkbox_api.md index 0d77c411c..40e8991af 100644 --- a/tdesign-component/example/assets/api/checkbox_api.md +++ b/tdesign-component/example/assets/api/checkbox_api.md @@ -7,16 +7,16 @@ | backgroundColor | Color? | - | 背景颜色 | | cardMode | bool | false | 展示为卡片模式 | | checkBoxLeftSpace | double? | - | 选项框左侧间距 | -| checked | bool | false | 选中状态。默认为`false` | +| checked | bool | false | 选中状态。默认为`false` 当FuiCheckBox嵌入到FuiCheckBoxGroup的时候,这个值表示初始状态,后续的状态会由Group管理 | | contentDirection | TContentDirection | TContentDirection.right | 文字相对icon的方位 | | customContentBuilder | ContentBuilder? | - | 完全自定义内容 | | customIconBuilder | IconBuilder? | - | 自定义Checkbox显示样式 | | customSpace | EdgeInsetsGeometry? | - | 自定义组件间距 | | disableColor | Color? | - | 禁用选择颜色 | | enable | bool | true | 不可用 | -| id | String? | - | id | +| id | String? | - | id 当FuiCheckBox嵌入到FuiCheckBoxGroup内时,这个值需要赋值,否则不会被纳入Group管理 | | insetSpacing | double? | 16 | 文字和非图标侧的距离 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onCheckBoxChanged | OnCheckValueChanged? | - | 切换监听 | | selectColor | Color? | - | 选择颜色 | | showDivider | bool | true | 是否展示分割线 | @@ -32,8 +32,6 @@ | titleFont | Font? | - | 标题字体大小 | | titleMaxLine | int? | - | 标题的行数 | -``` -``` ### TCheckboxGroup #### 默认构造方法 @@ -41,15 +39,96 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | checkedIds | List? | - | 勾选的CheckBox id列表 | -| child | | - | | +| child | Widget | - | 可以是任意包含TCheckBox的容器,比如: ``` Row( children: [ TCheckBox(), TCheckBox(), ... ] ) ``` | | contentDirection | TContentDirection? | - | 文字相对icon的方位 | | controller | TCheckboxGroupController? | - | 可以通过控制器操作勾选状态 | | customContentBuilder | ContentBuilder? | - | CheckBox完全自定义内容 | | customIconBuilder | IconBuilder? | - | 自定义选择icon的样式 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | maxChecked | int? | - | 最多可以勾选多少 | | onChangeGroup | OnGroupChange? | - | 状态变化监听器 | | onOverloadChecked | VoidCallback? | - | 超过最大可勾选的个数 | | spacing | double? | - | CheckBoxicon和文字的距离 | | style | TCheckboxStyle? | - | CheckBox复选框样式:圆形或方形 | | titleMaxLine | int? | - | CheckBox标题的行数 | + + +### TCheckboxStyle +#### 简介 +选择框的样式 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| circle | - | +| square | - | +| check | - | + + +### TContentDirection +#### 简介 +内容相对icon的位置,上、下、左、右,默认内容在icon的右边 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| left | - | +| right | - | + + +### TCheckBoxSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | - | +| small | - | + + +### IconBuilder +#### 简介 +自定义Icon +#### 类型定义 + +```dart +typedef IconBuilder = Widget? Function(BuildContext context, bool checked); +``` + + +### ContentBuilder +#### 简介 +自定义Content +#### 类型定义 + +```dart +typedef ContentBuilder = Widget Function(BuildContext context, bool checked, String? content); +``` + + +### OnCheckValueChanged +#### 类型定义 + +```dart +typedef OnCheckValueChanged = void Function(bool selected); +``` + + +### OnGroupChange +#### 简介 +CheckBoxGroup变化监听器 +#### 类型定义 + +```dart +typedef OnGroupChange = void Function(List checkedIds); +``` + + +### OnCheckBoxGroupChange +#### 类型定义 + +```dart +typedef OnCheckBoxGroupChange = void Function(List ids); +``` diff --git a/tdesign-component/example/assets/api/collapse_api.md b/tdesign-component/example/assets/api/collapse_api.md index 28e0ebbd2..bc0ef877e 100644 --- a/tdesign-component/example/assets/api/collapse_api.md +++ b/tdesign-component/example/assets/api/collapse_api.md @@ -9,13 +9,39 @@ | animationDuration | Duration | kThemeAnimationDuration | 折叠面板列表的动画时长 | | children | List | - | 折叠面板列表的子组件 | | elevation | double | 0 | 折叠面板列表的阴影 | -| expansionCallback | ExpansionPanelCallback? | - | 折叠面板列表的回调函数; | -| key | | - | | -| style | TCollapseStyle | TCollapseStyle.block | 折叠面板列表的样式 | +| expansionCallback | ExpansionPanelCallback? | - | 折叠面板列表的回调函数; 回调时,入参为当前点击的折叠面板的索引 index 和是否展开的状态 isExpanded | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| style | TCollapseStyle | TCollapseStyle.block | 折叠面板列表的样式 - [TCollapseStyle.block] 通栏风格 - [TCollapseStyle.card] 卡片风格 | + +#### 公开属性 + +| 属性 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| initialOpenPanelValue | Object? | - | 折叠面板列表的默认展开面板的值; 当使用 [TCollapse.accordion] 时,此值生效 | #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TCollapse.accordion | | +##### TCollapse.accordion + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| children | List | - | 折叠面板列表的子组件 | +| style | TCollapseStyle | TCollapseStyle.block | 折叠面板列表的样式 - [TCollapseStyle.block] 通栏风格 - [TCollapseStyle.card] 卡片风格 | +| expansionCallback | ExpansionPanelCallback? | - | 折叠面板列表的回调函数; 回调时,入参为当前点击的折叠面板的索引 index 和是否展开的状态 isExpanded | +| animationDuration | Duration | kThemeAnimationDuration | 折叠面板列表的动画时长 | +| elevation | double | 0 | 折叠面板列表的阴影 | +| initialOpenPanelValue | Object? | - | 折叠面板列表的默认展开面板的值; 当使用 [TCollapse.accordion] 时,此值生效 | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | + + +### TCollapseStyle +#### 简介 +折叠面板的组件样式 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| block | Block 通栏风格 | +| card | Card 卡片风格 | diff --git a/tdesign-component/example/assets/api/dialog_api.md b/tdesign-component/example/assets/api/dialog_api.md index 9dd84999d..0679ea786 100644 --- a/tdesign-component/example/assets/api/dialog_api.md +++ b/tdesign-component/example/assets/api/dialog_api.md @@ -5,19 +5,19 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | backgroundColor | Color? | - | 背景颜色 | -| buttonStyle | | TDialogButtonStyle.normal | | +| buttonStyle | TDialogButtonStyle | TDialogButtonStyle.normal | - | | buttonWidget | Widget? | - | 自定义按钮 | | content | String? | - | 内容 | | contentColor | Color? | - | 内容颜色 | | contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 | | contentWidget | Widget? | - | 内容Widget | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftBtn | TDialogButtonOptions? | - | 左侧按钮配置 | -| leftBtnAction | Function()? | - | 左侧按钮默认点击 | +| leftBtnAction | Function()? | - | 左侧按钮默认点击 | | padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 | | radius | double | 12.0 | 圆角 | | rightBtn | TDialogButtonOptions? | - | 右侧按钮配置 | -| rightBtnAction | Function()? | - | 右侧按钮默认点击 | +| rightBtnAction | Function()? | - | 右侧按钮默认点击 | | showCloseButton | bool? | - | 显示右上角关闭按钮 | | title | String? | - | 标题 | | titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | @@ -26,21 +26,36 @@ #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TAlertDialog.vertical | 纵向按钮排列的对话框 +##### TAlertDialog.vertical - [buttons]参数是必须的,纵向按钮默认样式都是[TButtonTheme.primary] | +纵向按钮排列的对话框 + + [buttons]参数是必须的,纵向按钮默认样式都是[TButtonTheme.primary] + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| buttons | List | - | - | +| backgroundColor | Color? | - | 背景颜色 | +| radius | double | 12.0 | 圆角 | +| title | String? | - | 标题 | +| titleColor | Color? | - | 标题颜色 | +| titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | +| contentWidget | Widget? | - | 内容Widget | +| content | String? | - | 内容 | +| contentColor | Color? | - | 内容颜色 | +| contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 | +| showCloseButton | bool? | - | 显示右上角关闭按钮 | +| padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 | +| buttonWidget | Widget? | - | 自定义按钮 | -``` -``` ### TConfirmDialog #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| action | Function()? | - | 点击 | +| action | Function()? | - | 点击 | | backgroundColor | Color? | - | 背景颜色 | | buttonStyle | TDialogButtonStyle | TDialogButtonStyle.normal | 按钮样式 | | buttonStyleCustom | TButtonStyle? | - | 按钮自定义样式属性,背景色、边框... | @@ -51,35 +66,31 @@ | contentColor | Color? | - | 内容颜色 | | contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 | | contentWidget | Widget? | - | 内容Widget | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 | | radius | double | 12.0 | 圆角 | | showCloseButton | bool? | - | 右上角关闭按钮 | | title | String? | - | 标题 | | titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | | titleColor | Color? | - | 标题颜色 | -| width | | - | | +| width | double? | - | - | -``` -``` ### TDialogButtonOptions #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| action | Function()? | - | 点击操作 | +| action | Function()? | - | 点击操作 | | fontWeight | FontWeight? | - | 字体粗细 | -| height | double? | - | 按钮高度 | -| style | TButtonStyle? | - | 按钮样式 | +| height | double? | - | 按钮高度 建议使用默认高度 | +| style | TButtonStyle? | - | 按钮样式 设置单个按钮的样式会覆盖Dialog的默认样式 | | theme | TButtonTheme? | - | 按钮类型 | | title | String | - | 标题内容 | | titleColor | Color? | - | 标题颜色 | | titleSize | double? | - | 字体大小 | | type | TButtonType? | - | 按钮类型 | -``` -``` ### TDialogScaffold #### 默认构造方法 @@ -88,25 +99,21 @@ | --- | --- | --- | --- | | backgroundColor | Color? | - | 背景色 | | body | Widget | - | Dialog主体 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | radius | double | 12.0 | 圆角 | | showCloseButton | bool? | - | 显示右上角关闭按钮 | | width | double? | - | 弹窗宽度 | -``` -``` ### TDialogTitle #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | title | String? | - | 标题文字 | | titleColor | Color? | - | 标题颜色 | -``` -``` ### TDialogContent #### 默认构造方法 @@ -115,10 +122,8 @@ | --- | --- | --- | --- | | content | String? | - | 标题文字 | | contentColor | Color? | - | 标题颜色 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | -``` -``` ### TDialogInfoWidget #### 默认构造方法 @@ -129,38 +134,32 @@ | contentColor | Color? | - | 内容颜色 | | contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 | | contentWidget | Widget? | - | 内容Widget | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | padding | EdgeInsetsGeometry? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容的内边距 | | title | String? | - | 标题 | | titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | | titleColor | Color? | - | 标题颜色 | -``` -``` ### HorizontalNormalButtons #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftBtn | TDialogButtonOptions | - | 左按钮 | | rightBtn | TDialogButtonOptions | - | 右按钮 | -``` -``` ### HorizontalTextButtons #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftBtn | TDialogButtonOptions | - | 左按钮 | | rightBtn | TDialogButtonOptions | - | 右按钮 | -``` -``` ### TDialogButton #### 默认构造方法 @@ -176,12 +175,10 @@ | buttonType | TButtonType? | - | 按钮类型 | | height | double? | 40.0 | 按钮高度 | | isBlock | bool | true | 按钮高度 | -| key | | - | | -| onPressed | Function() | - | 点击 | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| onPressed | Function() | - | 点击 | | width | double? | - | 按钮宽度 | -``` -``` ### TImageDialog #### 默认构造方法 @@ -195,7 +192,7 @@ | contentWidget | Widget? | - | 内容Widget | | image | Image | - | 图片 | | imagePosition | TDialogImagePosition? | TDialogImagePosition.top | 图片位置 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftBtn | TDialogButtonOptions? | - | 左侧按钮配置 | | padding | EdgeInsets? | - | 内容内边距 | | radius | double | 12.0 | 圆角 | @@ -205,8 +202,6 @@ | titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | | titleColor | Color? | - | 标题颜色 | -``` -``` ### TInputDialog #### 默认构造方法 @@ -220,7 +215,7 @@ | contentWidget | Widget? | - | 内容Widget | | customInputWidget | Widget? | - | 自定义输入框 | | hintText | String? | '' | 输入提示 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftBtn | TDialogButtonOptions? | - | 左侧按钮配置 | | padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 | | radius | double | 12.0 | 圆角 | @@ -230,3 +225,28 @@ | title | String? | - | 标题 | | titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | | titleColor | Color? | - | 标题颜色 | + + +### TDialogButtonStyle +#### 简介 +Dialog按钮样式 + + 用于在Dialog层面配置按钮样式 + Dialog内支持配置每个按钮的样式 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| normal | - | +| text | - | + + +### TDialogImagePosition +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| top | - | +| middle | - | diff --git a/tdesign-component/example/assets/api/divider_api.md b/tdesign-component/example/assets/api/divider_api.md index 19030afc5..72e5ed264 100644 --- a/tdesign-component/example/assets/api/divider_api.md +++ b/tdesign-component/example/assets/api/divider_api.md @@ -11,9 +11,20 @@ | height | double | 0.5 | 高度,横向线条使用 | | hideLine | bool | false | 隐藏线条,使用纯文本分割 | | isDashed | bool | false | 是否为虚线 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | margin | EdgeInsetsGeometry? | - | 外部填充 | | text | String? | - | 文本字符串,使用默认样式 | | textStyle | TextStyle? | - | 自定义文本样式 | | widget | Widget? | - | 中间控件,可自定义样式 | | width | double? | - | 宽度,需要竖向线条时使用 | + + +### TextAlignment +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| left | - | +| center | - | +| right | - | diff --git a/tdesign-component/example/assets/api/drawer_api.md b/tdesign-component/example/assets/api/drawer_api.md index 6dcef5beb..9ca2934fa 100644 --- a/tdesign-component/example/assets/api/drawer_api.md +++ b/tdesign-component/example/assets/api/drawer_api.md @@ -6,11 +6,11 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | +| context | BuildContext | - | 上下文 | | backgroundColor | Color? | - | 组件背景颜色 | | bordered | bool? | true | 是否显示边框 | | closeOnOverlayClick | bool? | true | 点击蒙层时是否关闭抽屉 | | contentWidget | Widget? | - | 自定义内容,优先级高于[items]/[footer]/[title] | -| context | BuildContext | context | 上下文 | | drawerTop | double? | - | 距离顶部的距离 | | footer | Widget? | - | 抽屉的底部 | | hover | bool? | true | 是否开启点击反馈 | @@ -26,8 +26,6 @@ | visible | bool? | - | 组件是否可见 | | width | double? | 280 | 宽度 | -``` -``` ### TDrawerWidget #### 简介 @@ -44,15 +42,13 @@ | hover | bool? | true | 是否开启点击反馈 | | isShowLastBordered | bool? | true | 是否显示最后一行分割线 | | items | List? | - | 抽屉里的列表项 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onItemClick | TDrawerItemClickCallback? | - | 点击抽屉里的列表项触发 | | style | TCellStyle? | - | 列表自定义样式 | | title | String? | - | 抽屉的标题 | | titleWidget | Widget? | - | 抽屉的标题组件 | | width | double? | 280 | 宽度 | -``` -``` ### TDrawerItem #### 简介 @@ -64,3 +60,23 @@ | content | Widget? | - | 完全自定义 | | icon | Widget? | - | 每列图标 | | title | String? | - | 每列标题 | + + +### TDrawerPlacement +#### 简介 +抽屉方向 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| left | - | +| right | - | + + +### TDrawerItemClickCallback +#### 类型定义 + +```dart +typedef TDrawerItemClickCallback = void Function(int index, TDrawerItem item); +``` diff --git a/tdesign-component/example/assets/api/dropdown-menu_api.md b/tdesign-component/example/assets/api/dropdown-menu_api.md index e19bc36b7..940cadce5 100644 --- a/tdesign-component/example/assets/api/dropdown-menu_api.md +++ b/tdesign-component/example/assets/api/dropdown-menu_api.md @@ -16,7 +16,7 @@ | height | double? | 48 | menu的高度 | | isScrollable | bool? | false | 是否开启滚动列表 | | items | List? | - | 下拉菜单 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | labelBuilder | LabelBuilder? | - | 自定义标签内容 | | onMenuClosed | ValueChanged? | - | 关闭菜单事件 | | onMenuOpened | ValueChanged? | - | 展开菜单事件 | @@ -24,8 +24,6 @@ | tabBarAlign | MainAxisAlignment? | MainAxisAlignment.center | [TDropdownItem.label]和[arrowIcon]/[TDropdownItem.arrowIcon]的对齐方式 | | width | double? | - | menu的宽度 | -``` -``` ### TDropdownItem #### 简介 @@ -39,7 +37,7 @@ | builder | TDropdownItemContentBuilder? | - | 完全自定义展示内容 | | controller | TDropdownItemController? | - | 下拉菜单控制器 | | disabled | bool? | false | 是否禁用 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | label | String? | - | 标题 | | maxHeight | double? | - | 内容最大高度 | | minHeight | double? | - | 内容最小高度 | @@ -53,8 +51,12 @@ | tabBarFlex | int? | 1 | 该item在menu上的宽度占比,仅在[TDropdownMenu.isScrollable]为false时有效 | | tabBarWidth | double? | - | 该item在menu上的宽度,仅在[TDropdownMenu.isScrollable]为true时有效 | -``` -``` +#### 静态成员 + +| 名称 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| operateHeight | double | - | - | + ### TDropdownItemOption #### 简介 @@ -71,9 +73,55 @@ | selectedColor | Color? | - | 选中颜色 | | value | String | - | 选项值 | + +### TDropdownItemController +#### 简介 +下拉菜单控制器 + +### TDropdownMenuDirection +#### 简介 +菜单展开方向 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| down | 向下 | +| up | 向上 | +| auto | 根据内容高度动态展示方向 | + + +### TDropdownItemContentBuilder +#### 类型定义 + +```dart +typedef TDropdownItemContentBuilder = Widget Function(BuildContext context, _TDropdownItemState itemState, TDropdownPopup? popupState); ``` + + +### TDropdownItemOptionsCallback +#### 类型定义 + +```dart +typedef TDropdownItemOptionsCallback = void Function(List? options); ``` -### TDropdownItemController + +### TDropdownItemBuilder #### 简介 -下拉菜单控制器 \ No newline at end of file +下拉菜单构建器 +#### 类型定义 + +```dart +typedef TDropdownItemBuilder = List Function(BuildContext context); +``` + + +### LabelBuilder +#### 简介 +自定义标签内容 +#### 类型定义 + +```dart +typedef LabelBuilder = Widget Function(BuildContext context, String label, bool isOpened, int index); +``` diff --git a/tdesign-component/example/assets/api/empty_api.md b/tdesign-component/example/assets/api/empty_api.md index e39301b33..ba743b257 100644 --- a/tdesign-component/example/assets/api/empty_api.md +++ b/tdesign-component/example/assets/api/empty_api.md @@ -10,8 +10,26 @@ | emptyTextFont | Font? | - | 描述文字大小 | | icon | IconData? | TIcons.info_circle_filled | 图标 | | image | Widget? | - | 展示图片 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onTapEvent | TTapEvent? | - | 点击事件 | | operationText | String? | - | 操作按钮文案 | | operationTheme | TButtonTheme? | - | 操作按钮文案主题色 | | type | TEmptyType | TEmptyType.plain | 类型,为operation有操作按钮,plain无按钮 | + + +### TEmptyType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| plain | - | +| operation | - | + + +### TTapEvent +#### 类型定义 + +```dart +typedef TTapEvent = void Function(); +``` diff --git a/tdesign-component/example/assets/api/fab_api.md b/tdesign-component/example/assets/api/fab_api.md index 641de8127..405f3c9fd 100644 --- a/tdesign-component/example/assets/api/fab_api.md +++ b/tdesign-component/example/assets/api/fab_api.md @@ -5,9 +5,43 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | icon | Icon? | - | 图标 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onClick | VoidCallback? | - | 点击事件 | | shape | TFabShape | TFabShape.circle | 形状 | | size | TFabSize | TFabSize.large | 大小 | | text | String? | - | 文本 | | theme | TFabTheme | TFabTheme.defaultTheme | 主题 | + + +### TFabTheme +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| primary | - | +| defaultTheme | - | +| light | - | +| danger | - | + + +### TFabShape +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| circle | - | +| square | - | + + +### TFabSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | - | +| medium | - | +| small | - | +| extraSmall | - | diff --git a/tdesign-component/example/assets/api/footer_api.md b/tdesign-component/example/assets/api/footer_api.md index 17deb0158..f5df55732 100644 --- a/tdesign-component/example/assets/api/footer_api.md +++ b/tdesign-component/example/assets/api/footer_api.md @@ -4,10 +4,21 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | +| type | TFooterType | - | 样式 | | height | double? | - | 自定义图片高 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | links | List | const [] | 链接 | | logo | String? | - | 品牌图片 | | text | String | '' | 文字 | -| type | TFooterType | type | 样式 | | width | double? | - | 自定义图片宽 | + + +### TFooterType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| text | 文字样式 | +| link | 链接样式 | +| brand | 品牌样式 | diff --git a/tdesign-component/example/assets/api/form_api.md b/tdesign-component/example/assets/api/form_api.md index 61922773d..948830f5e 100644 --- a/tdesign-component/example/assets/api/form_api.md +++ b/tdesign-component/example/assets/api/form_api.md @@ -9,24 +9,22 @@ | data | Map | - | 表单数据 | | disabled | bool | false | 是否禁用整个表单 | | errorMessage | Object? | - | 表单信息错误信息配置 | -| formContentAlign | TextAlign | TextAlign.left | 表单内容对齐方式: 左对齐、右对齐、居中对齐 | +| formContentAlign | TextAlign | TextAlign.left | 表单内容对齐方式: 左对齐、右对齐、居中对齐 可选项: left/right/center 默认为左对齐 优先级低于 TFormItem 的对齐 API TODO: TStepper TRate 等组件没用实现通用性 | | formController | FormController? | - | 表单控制器 | -| formLabelAlign | TextAlign? | TextAlign.left | 表单字段标签的对齐方式: | -| formShowErrorMessage | bool? | true | 校验不通过时,是否显示错误提示信息,统一控制全部表单项 | +| formLabelAlign | TextAlign? | TextAlign.left | 表单字段标签的对齐方式: 左对齐、右对齐、顶部对齐 可选项: left/right/top TODO: 表单总体标签对齐方式 | +| formShowErrorMessage | bool? | true | 校验不通过时,是否显示错误提示信息,统一控制全部表单项 如果希望控制单个表单项,请给 FormItem 设置该属性 | | isHorizontal | bool | true | 表单排列方式是否为 水平方向 | | items | List | - | 表单内容 items | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | labelWidth | double? | 20.0 | 可以整体设置 label 标签宽度 | | onReset | Function? | - | 表单重置时触发 | | onSubmit | Function | - | 表单提交时触发 | -| preventSubmitDefault | bool? | true | 是否阻止表单提交默认事件(表单提交默认事件会刷新页面) | +| preventSubmitDefault | bool? | true | 是否阻止表单提交默认事件(表单提交默认事件会刷新页面) 设置为 true 可以避免刷新 | | requiredMark | bool? | true | 是否显示必填符号(*),默认显示 | | rules | Map | - | 整个表单字段校验规则 | -| scrollToFirstError | String? | - | 表单校验不通过时,是否自动滚动到第一个校验不通过的字段,平滑滚动或是瞬间直达。 | +| scrollToFirstError | String? | - | 表单校验不通过时,是否自动滚动到第一个校验不通过的字段,平滑滚动或是瞬间直达。 值为空则表示不滚动。可选项:''/smooth/auto | | submitWithWarningMessage | bool? | false | 【讨论中】当校验结果只有告警信息时,是否触发 submit 提交事件 | -``` -``` ### TFormItem #### 默认构造方法 @@ -36,20 +34,20 @@ | additionInfo | String? | - | TInput的辅助信息 | | backgroundColor | Color? | - | 背景色 | | child | Widget? | - | 表单子组件 | -| contentAlign | TextAlign? | - | 表单显示内容对齐方式: | -| formItemNotifier | | - | | +| contentAlign | TextAlign? | - | 表单显示内容对齐方式: left、right、top TODO: TStepper TRate 等组件没用实现通用性 | +| formItemNotifier | FormItemNotifier? | - | - | | formRules | List? | - | 整个表单的校验规则 | | help | String? | - | TInput 默认显示文字 | -| hintText | null | '' | 提示内容 | +| hintText | - | '' | 提示内容 | | indicator | bool? | - | TTextarea 的属性,指示器 | | itemRule | List? | - | 表单项验证规则 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | label | String? | - | 表单项标签左侧展示的内容 | -| labelAlign | TextAlign? | - | TODO: item 标签对齐方式 | +| labelAlign | TextAlign? | - | TODO: item 标签对齐方式 可选: left、right、top | | labelWidget | Widget? | - | 自定义标签 | | labelWidth | double? | - | 标签宽度,如果提供则覆盖Form的labelWidth | | name | String? | - | 表单字段名称 | -| radios | | - | | +| radios | Map? | - | - | | requiredMark | bool? | true | 是否显示必填标记(*) | | select | String | '' | 选择器 适用于日期选择器等 | | selectFn | Function? | - | 选择器方法 适用于日期选择器等 | @@ -57,8 +55,6 @@ | tipAlign | TextAlign? | - | 组件提示内容对齐方式 | | type | TFormItemType | - | 表格单元需要使用的组件类型 | -``` -``` ### TFormValidation #### 默认构造方法 @@ -68,3 +64,21 @@ | errorMessage | String | - | 错误提示信息 | | type | TFormItemType | - | 校验对象的类型 | | validate | String? Function(dynamic) | - | 校验方法 | + + +### TFormItemType +#### 简介 +表格单元选用组件类型的枚举 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| input | - | +| radios | - | +| dateTimePicker | - | +| cascader | - | +| stepper | - | +| rate | - | +| textarea | - | +| upLoadImg | - | diff --git a/tdesign-component/example/assets/api/icon_api.md b/tdesign-component/example/assets/api/icon_api.md index 74dbb5f5b..73c5f852d 100644 --- a/tdesign-component/example/assets/api/icon_api.md +++ b/tdesign-component/example/assets/api/icon_api.md @@ -1,8 +1,2128 @@ ## API ### TIcons +#### 静态成员 + +| 名称 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| accessibility | - | - | - | +| accessibility_filled | - | - | - | +| activity | - | - | - | +| activity_filled | - | - | - | +| add | - | - | - | +| add_and_subtract | - | - | - | +| add_circle | - | - | - | +| add_circle_filled | - | - | - | +| add_rectangle | - | - | - | +| add_rectangle_filled | - | - | - | +| address_book | - | - | - | +| address_book_filled | - | - | - | +| adjustment | - | - | - | +| adjustment_filled | - | - | - | +| airplay_wave | - | - | - | +| airplay_wave_filled | - | - | - | +| alarm | - | - | - | +| alarm_add | - | - | - | +| alarm_add_filled | - | - | - | +| alarm_filled | - | - | - | +| alarm_off | - | - | - | +| alarm_off_filled | - | - | - | +| align_top | - | - | - | +| align_vertical | - | - | - | +| all | - | - | - | +| alpha | - | - | - | +| analytics | - | - | - | +| analytics_filled | - | - | - | +| anchor | - | - | - | +| angry | - | - | - | +| angry_filled | - | - | - | +| animation | - | - | - | +| animation_1 | - | - | - | +| animation_1_filled | - | - | - | +| animation_filled | - | - | - | +| anticlockwise | - | - | - | +| anticlockwise_filled | - | - | - | +| api | - | - | - | +| app | - | - | - | +| app_filled | - | - | - | +| apple | - | - | - | +| apple_filled | - | - | - | +| application | - | - | - | +| application_filled | - | - | - | +| architecture_hui_style | - | - | - | +| architecture_hui_style_filled | - | - | - | +| archway | - | - | - | +| archway_1 | - | - | - | +| archway_1_filled | - | - | - | +| archway_filled | - | - | - | +| arrow_down | - | - | - | +| arrow_down_circle | - | - | - | +| arrow_down_circle_filled | - | - | - | +| arrow_down_rectangle | - | - | - | +| arrow_down_rectangle_filled | - | - | - | +| arrow_left | - | - | - | +| arrow_left_circle | - | - | - | +| arrow_left_circle_filled | - | - | - | +| arrow_left_down | - | - | - | +| arrow_left_down_circle | - | - | - | +| arrow_left_down_circle_filled | - | - | - | +| arrow_left_right_1 | - | - | - | +| arrow_left_right_2 | - | - | - | +| arrow_left_right_3 | - | - | - | +| arrow_left_right_circle | - | - | - | +| arrow_left_right_circle_filled | - | - | - | +| arrow_left_up | - | - | - | +| arrow_left_up_circle | - | - | - | +| arrow_left_up_circle_filled | - | - | - | +| arrow_right | - | - | - | +| arrow_right_circle | - | - | - | +| arrow_right_circle_filled | - | - | - | +| arrow_right_down | - | - | - | +| arrow_right_down_circle | - | - | - | +| arrow_right_down_circle_filled | - | - | - | +| arrow_right_up | - | - | - | +| arrow_right_up_circle | - | - | - | +| arrow_right_up_circle_filled | - | - | - | +| arrow_triangle_down | - | - | - | +| arrow_triangle_down_filled | - | - | - | +| arrow_triangle_up | - | - | - | +| arrow_triangle_up_filled | - | - | - | +| arrow_up | - | - | - | +| arrow_up_circle | - | - | - | +| arrow_up_circle_filled | - | - | - | +| arrow_up_down_1 | - | - | - | +| arrow_up_down_2 | - | - | - | +| arrow_up_down_3 | - | - | - | +| arrow_up_down_circle | - | - | - | +| arrow_up_down_circle_filled | - | - | - | +| artboard | - | - | - | +| article | - | - | - | +| article_filled | - | - | - | +| assignment | - | - | - | +| assignment_checked | - | - | - | +| assignment_checked_filled | - | - | - | +| assignment_code | - | - | - | +| assignment_code_filled | - | - | - | +| assignment_error | - | - | - | +| assignment_error_filled | - | - | - | +| assignment_filled | - | - | - | +| assignment_user | - | - | - | +| assignment_user_filled | - | - | - | +| attach | - | - | - | +| attic | - | - | - | +| attic_1 | - | - | - | +| attic_1_filled | - | - | - | +| attic_filled | - | - | - | +| audio | - | - | - | +| audio_filled | - | - | - | +| awkward | - | - | - | +| awkward_filled | - | - | - | +| backtop | - | - | - | +| backtop_rectangle | - | - | - | +| backtop_rectangle_filled | - | - | - | +| backup | - | - | - | +| backup_filled | - | - | - | +| backward | - | - | - | +| backward_filled | - | - | - | +| bad_laugh | - | - | - | +| bad_laugh_filled | - | - | - | +| bamboo_shoot | - | - | - | +| bamboo_shoot_filled | - | - | - | +| banana | - | - | - | +| banana_filled | - | - | - | +| barbecue | - | - | - | +| barbecue_filled | - | - | - | +| barcode | - | - | - | +| barcode_1 | - | - | - | +| base_station | - | - | - | +| battery | - | - | - | +| battery_add | - | - | - | +| battery_add_filled | - | - | - | +| battery_charging | - | - | - | +| battery_charging_filled | - | - | - | +| battery_filled | - | - | - | +| battery_low | - | - | - | +| battery_low_filled | - | - | - | +| bean | - | - | - | +| bean_filled | - | - | - | +| beer | - | - | - | +| beer_filled | - | - | - | +| beta | - | - | - | +| bifurcate | - | - | - | +| bifurcate_filled | - | - | - | +| bill | - | - | - | +| bill_filled | - | - | - | +| bluetooth | - | - | - | +| bone | - | - | - | +| bone_filled | - | - | - | +| book | - | - | - | +| book_filled | - | - | - | +| book_open | - | - | - | +| book_open_filled | - | - | - | +| book_unknown | - | - | - | +| book_unknown_filled | - | - | - | +| bookmark | - | - | - | +| bookmark_add | - | - | - | +| bookmark_add_filled | - | - | - | +| bookmark_checked | - | - | - | +| bookmark_checked_filled | - | - | - | +| bookmark_double | - | - | - | +| bookmark_double_filled | - | - | - | +| bookmark_filled | - | - | - | +| bookmark_minus | - | - | - | +| bookmark_minus_filled | - | - | - | +| braces | - | - | - | +| brackets | - | - | - | +| bread | - | - | - | +| bread_filled | - | - | - | +| bridge | - | - | - | +| bridge_1 | - | - | - | +| bridge_1_filled | - | - | - | +| bridge_2 | - | - | - | +| bridge_2_filled | - | - | - | +| bridge_3 | - | - | - | +| bridge_4 | - | - | - | +| bridge_5 | - | - | - | +| bridge_5_filled | - | - | - | +| bridge_6 | - | - | - | +| bridge_6_filled | - | - | - | +| brightness | - | - | - | +| brightness_1 | - | - | - | +| brightness_1_filled | - | - | - | +| brightness_filled | - | - | - | +| broccoli | - | - | - | +| broccoli_filled | - | - | - | +| browse | - | - | - | +| browse_filled | - | - | - | +| browse_gallery | - | - | - | +| browse_gallery_filled | - | - | - | +| browse_off | - | - | - | +| browse_off_filled | - | - | - | +| brush | - | - | - | +| brush_filled | - | - | - | +| bug | - | - | - | +| bug_filled | - | - | - | +| bug_report | - | - | - | +| bug_report_filled | - | - | - | +| building | - | - | - | +| building_1 | - | - | - | +| building_1_filled | - | - | - | +| building_2 | - | - | - | +| building_2_filled | - | - | - | +| building_3 | - | - | - | +| building_3_filled | - | - | - | +| building_4 | - | - | - | +| building_4_filled | - | - | - | +| building_5 | - | - | - | +| building_5_filled | - | - | - | +| building_filled | - | - | - | +| bulletpoint | - | - | - | +| button | - | - | - | +| button_filled | - | - | - | +| cabbage | - | - | - | +| cabbage_filled | - | - | - | +| cake | - | - | - | +| cake_filled | - | - | - | +| calculation | - | - | - | +| calculation_1 | - | - | - | +| calculation_1_filled | - | - | - | +| calculator | - | - | - | +| calculator_1 | - | - | - | +| calculator_filled | - | - | - | +| calendar | - | - | - | +| calendar_1 | - | - | - | +| calendar_1_filled | - | - | - | +| calendar_2 | - | - | - | +| calendar_2_filled | - | - | - | +| calendar_edit | - | - | - | +| calendar_edit_filled | - | - | - | +| calendar_event | - | - | - | +| calendar_event_filled | - | - | - | +| calendar_filled | - | - | - | +| call | - | - | - | +| call_1 | - | - | - | +| call_1_filled | - | - | - | +| call_cancel | - | - | - | +| call_cancel_filled | - | - | - | +| call_filled | - | - | - | +| call_forwarded | - | - | - | +| call_forwarded_filled | - | - | - | +| call_incoming | - | - | - | +| call_incoming_filled | - | - | - | +| call_off | - | - | - | +| call_off_filled | - | - | - | +| calm | - | - | - | +| calm_1 | - | - | - | +| calm_1_filled | - | - | - | +| calm_filled | - | - | - | +| camera | - | - | - | +| camera_1 | - | - | - | +| camera_1_filled | - | - | - | +| camera_2 | - | - | - | +| camera_2_filled | - | - | - | +| camera_filled | - | - | - | +| camera_off | - | - | - | +| camera_off_filled | - | - | - | +| candy | - | - | - | +| candy_filled | - | - | - | +| card | - | - | - | +| card_filled | - | - | - | +| cardmembership | - | - | - | +| cardmembership_filled | - | - | - | +| caret_down | - | - | - | +| caret_down_small | - | - | - | +| caret_left | - | - | - | +| caret_left_small | - | - | - | +| caret_right | - | - | - | +| caret_right_small | - | - | - | +| caret_up | - | - | - | +| caret_up_small | - | - | - | +| cart | - | - | - | +| cart_add | - | - | - | +| cart_add_filled | - | - | - | +| cart_filled | - | - | - | +| cast | - | - | - | +| cast_filled | - | - | - | +| castle | - | - | - | +| castle_1 | - | - | - | +| castle_1_filled | - | - | - | +| castle_2 | - | - | - | +| castle_2_filled | - | - | - | +| castle_3 | - | - | - | +| castle_3_filled | - | - | - | +| castle_4 | - | - | - | +| castle_4_filled | - | - | - | +| castle_5 | - | - | - | +| castle_5_filled | - | - | - | +| castle_6 | - | - | - | +| castle_6_filled | - | - | - | +| castle_7 | - | - | - | +| castle_7_filled | - | - | - | +| castle_filled | - | - | - | +| cat | - | - | - | +| cat_filled | - | - | - | +| catalog | - | - | - | +| catalog_filled | - | - | - | +| cd | - | - | - | +| cd_filled | - | - | - | +| celsius | - | - | - | +| center_focus_strong | - | - | - | +| center_focus_strong_filled | - | - | - | +| centimeter | - | - | - | +| certificate | - | - | - | +| certificate_1 | - | - | - | +| certificate_1_filled | - | - | - | +| certificate_filled | - | - | - | +| chart | - | - | - | +| chart_3d | - | - | - | +| chart_3d_filled | - | - | - | +| chart_add | - | - | - | +| chart_add_filled | - | - | - | +| chart_analytics | - | - | - | +| chart_area | - | - | - | +| chart_area_filled | - | - | - | +| chart_area_multi | - | - | - | +| chart_area_multi_filled | - | - | - | +| chart_bar | - | - | - | +| chart_bar_filled | - | - | - | +| chart_bubble | - | - | - | +| chart_bubble_filled | - | - | - | +| chart_column | - | - | - | +| chart_column_filled | - | - | - | +| chart_combo | - | - | - | +| chart_combo_filled | - | - | - | +| chart_filled | - | - | - | +| chart_line | - | - | - | +| chart_line_data | - | - | - | +| chart_line_data_1 | - | - | - | +| chart_line_multi | - | - | - | +| chart_maximum | - | - | - | +| chart_median | - | - | - | +| chart_minimum | - | - | - | +| chart_pie | - | - | - | +| chart_pie_filled | - | - | - | +| chart_radar | - | - | - | +| chart_radar_filled | - | - | - | +| chart_radial | - | - | - | +| chart_ring | - | - | - | +| chart_ring_1 | - | - | - | +| chart_ring_1_filled | - | - | - | +| chart_ring_filled | - | - | - | +| chart_scatter | - | - | - | +| chart_stacked | - | - | - | +| chart_stacked_filled | - | - | - | +| chat | - | - | - | +| chat_add | - | - | - | +| chat_add_filled | - | - | - | +| chat_bubble | - | - | - | +| chat_bubble_1 | - | - | - | +| chat_bubble_1_filled | - | - | - | +| chat_bubble_add | - | - | - | +| chat_bubble_add_filled | - | - | - | +| chat_bubble_error | - | - | - | +| chat_bubble_error_filled | - | - | - | +| chat_bubble_filled | - | - | - | +| chat_bubble_help | - | - | - | +| chat_bubble_help_filled | - | - | - | +| chat_bubble_history | - | - | - | +| chat_bubble_history_filled | - | - | - | +| chat_bubble_locked | - | - | - | +| chat_bubble_locked_filled | - | - | - | +| chat_bubble_smile | - | - | - | +| chat_bubble_smile_filled | - | - | - | +| chat_checked | - | - | - | +| chat_checked_filled | - | - | - | +| chat_clear | - | - | - | +| chat_clear_filled | - | - | - | +| chat_double | - | - | - | +| chat_double_filled | - | - | - | +| chat_error | - | - | - | +| chat_error_filled | - | - | - | +| chat_filled | - | - | - | +| chat_heart | - | - | - | +| chat_heart_filled | - | - | - | +| chat_message | - | - | - | +| chat_message_filled | - | - | - | +| chat_off | - | - | - | +| chat_off_filled | - | - | - | +| chat_poll | - | - | - | +| chat_poll_filled | - | - | - | +| chat_setting | - | - | - | +| chat_setting_filled | - | - | - | +| check | - | - | - | +| check_circle | - | - | - | +| check_circle_filled | - | - | - | +| check_double | - | - | - | +| check_rectangle | - | - | - | +| check_rectangle_filled | - | - | - | +| cheese | - | - | - | +| cheese_filled | - | - | - | +| cherry | - | - | - | +| cherry_filled | - | - | - | +| chevron_down | - | - | - | +| chevron_down_circle | - | - | - | +| chevron_down_circle_filled | - | - | - | +| chevron_down_double | - | - | - | +| chevron_down_double_s | - | - | - | +| chevron_down_rectangle | - | - | - | +| chevron_down_rectangle_filled | - | - | - | +| chevron_down_s | - | - | - | +| chevron_left | - | - | - | +| chevron_left_circle | - | - | - | +| chevron_left_circle_filled | - | - | - | +| chevron_left_double | - | - | - | +| chevron_left_double_s | - | - | - | +| chevron_left_rectangle | - | - | - | +| chevron_left_rectangle_filled | - | - | - | +| chevron_left_s | - | - | - | +| chevron_right | - | - | - | +| chevron_right_circle | - | - | - | +| chevron_right_circle_filled | - | - | - | +| chevron_right_double | - | - | - | +| chevron_right_double_s | - | - | - | +| chevron_right_rectangle | - | - | - | +| chevron_right_rectangle_filled | - | - | - | +| chevron_right_s | - | - | - | +| chevron_up | - | - | - | +| chevron_up_circle | - | - | - | +| chevron_up_circle_filled | - | - | - | +| chevron_up_double | - | - | - | +| chevron_up_double_s | - | - | - | +| chevron_up_rectangle | - | - | - | +| chevron_up_rectangle_filled | - | - | - | +| chevron_up_s | - | - | - | +| chicken | - | - | - | +| chili | - | - | - | +| chili_filled | - | - | - | +| chimney | - | - | - | +| chimney_1 | - | - | - | +| chimney_1_filled | - | - | - | +| chimney_2 | - | - | - | +| chimney_2_filled | - | - | - | +| chimney_filled | - | - | - | +| chinese_cabbage | - | - | - | +| chinese_cabbage_filled | - | - | - | +| church | - | - | - | +| church_filled | - | - | - | +| circle | - | - | - | +| circle_filled | - | - | - | +| city | - | - | - | +| city_1 | - | - | - | +| city_10 | - | - | - | +| city_10_filled | - | - | - | +| city_11 | - | - | - | +| city_11_filled | - | - | - | +| city_12 | - | - | - | +| city_12_filled | - | - | - | +| city_13 | - | - | - | +| city_13_filled | - | - | - | +| city_14 | - | - | - | +| city_14_filled | - | - | - | +| city_15 | - | - | - | +| city_15_filled | - | - | - | +| city_1_filled | - | - | - | +| city_2 | - | - | - | +| city_2_filled | - | - | - | +| city_3 | - | - | - | +| city_3_filled | - | - | - | +| city_4 | - | - | - | +| city_4_filled | - | - | - | +| city_5 | - | - | - | +| city_5_filled | - | - | - | +| city_6 | - | - | - | +| city_6_filled | - | - | - | +| city_7 | - | - | - | +| city_7_filled | - | - | - | +| city_8 | - | - | - | +| city_8_filled | - | - | - | +| city_9 | - | - | - | +| city_9_filled | - | - | - | +| city_ancient | - | - | - | +| city_ancient_1 | - | - | - | +| city_ancient_1_filled | - | - | - | +| city_ancient_2 | - | - | - | +| city_ancient_2_filled | - | - | - | +| city_ancient_filled | - | - | - | +| city_filled | - | - | - | +| clear | - | - | - | +| clear_filled | - | - | - | +| clear_formatting | - | - | - | +| clear_formatting_1 | - | - | - | +| clear_formatting_1_filled | - | - | - | +| clear_formatting_filled | - | - | - | +| close | - | - | - | +| close_circle | - | - | - | +| close_circle_filled | - | - | - | +| close_octagon | - | - | - | +| close_octagon_filled | - | - | - | +| close_rectangle | - | - | - | +| close_rectangle_filled | - | - | - | +| cloud | - | - | - | +| cloud_download | - | - | - | +| cloud_filled | - | - | - | +| cloud_upload | - | - | - | +| cloudy_day | - | - | - | +| cloudy_day_filled | - | - | - | +| cloudy_night | - | - | - | +| cloudy_night_filled | - | - | - | +| cloudy_night_rain | - | - | - | +| cloudy_night_rain_filled | - | - | - | +| cloudy_rain | - | - | - | +| cloudy_rain_filled | - | - | - | +| cloudy_sunny | - | - | - | +| cloudy_sunny_filled | - | - | - | +| code | - | - | - | +| code_1 | - | - | - | +| code_off | - | - | - | +| cola | - | - | - | +| cola_filled | - | - | - | +| collage | - | - | - | +| collage_filled | - | - | - | +| collection | - | - | - | +| collection_filled | - | - | - | +| color_invert | - | - | - | +| color_invert_filled | - | - | - | +| combination | - | - | - | +| combination_filled | - | - | - | +| command | - | - | - | +| compass | - | - | - | +| compass_1 | - | - | - | +| compass_1_filled | - | - | - | +| compass_filled | - | - | - | +| component_breadcrumb | - | - | - | +| component_breadcrumb_filled | - | - | - | +| component_checkbox | - | - | - | +| component_checkbox_filled | - | - | - | +| component_divider_horizontal | - | - | - | +| component_divider_horizontal_filled | - | - | - | +| component_divider_vertical | - | - | - | +| component_divider_vertical_filled | - | - | - | +| component_dropdown | - | - | - | +| component_dropdown_filled | - | - | - | +| component_grid | - | - | - | +| component_grid_filled | - | - | - | +| component_input | - | - | - | +| component_input_filled | - | - | - | +| component_layout | - | - | - | +| component_layout_filled | - | - | - | +| component_radio | - | - | - | +| component_space | - | - | - | +| component_space_filled | - | - | - | +| component_steps | - | - | - | +| component_steps_filled | - | - | - | +| component_switch | - | - | - | +| component_switch_filled | - | - | - | +| constraint | - | - | - | +| contrast | - | - | - | +| contrast_1 | - | - | - | +| contrast_1_filled | - | - | - | +| contrast_filled | - | - | - | +| control_platform | - | - | - | +| control_platform_filled | - | - | - | +| cooperate | - | - | - | +| cooperate_filled | - | - | - | +| coordinate_system | - | - | - | +| coordinate_system_filled | - | - | - | +| copy | - | - | - | +| copy_filled | - | - | - | +| copyright | - | - | - | +| copyright_filled | - | - | - | +| corn | - | - | - | +| corn_filled | - | - | - | +| coupon | - | - | - | +| coupon_filled | - | - | - | +| course | - | - | - | +| course_filled | - | - | - | +| cpu | - | - | - | +| cpu_filled | - | - | - | +| crack | - | - | - | +| crack_filled | - | - | - | +| creditcard | - | - | - | +| creditcard_add | - | - | - | +| creditcard_add_filled | - | - | - | +| creditcard_filled | - | - | - | +| creditcard_off | - | - | - | +| creditcard_off_filled | - | - | - | +| crooked_smile | - | - | - | +| crooked_smile_filled | - | - | - | +| cry_and_laugh | - | - | - | +| cry_and_laugh_filled | - | - | - | +| cry_loudly | - | - | - | +| cry_loudly_filled | - | - | - | +| css3 | - | - | - | +| css3_filled | - | - | - | +| cucumber | - | - | - | +| currency_exchange | - | - | - | +| cursor | - | - | - | +| cursor_filled | - | - | - | +| curtain | - | - | - | +| curtain_filled | - | - | - | +| curve | - | - | - | +| cut | - | - | - | +| cut_1 | - | - | - | +| dam | - | - | - | +| dam_1 | - | - | - | +| dam_1_filled | - | - | - | +| dam_2 | - | - | - | +| dam_2_filled | - | - | - | +| dam_3 | - | - | - | +| dam_3_filled | - | - | - | +| dam_4 | - | - | - | +| dam_4_filled | - | - | - | +| dam_5 | - | - | - | +| dam_5_filled | - | - | - | +| dam_6 | - | - | - | +| dam_6_filled | - | - | - | +| dam_7 | - | - | - | +| dam_7_filled | - | - | - | +| dam_filled | - | - | - | +| dart_board | - | - | - | +| dart_board_filled | - | - | - | +| dashboard | - | - | - | +| dashboard_1 | - | - | - | +| dashboard_1_filled | - | - | - | +| dashboard_filled | - | - | - | +| data | - | - | - | +| data_base | - | - | - | +| data_base_filled | - | - | - | +| data_checked | - | - | - | +| data_checked_filled | - | - | - | +| data_display | - | - | - | +| data_error | - | - | - | +| data_error_filled | - | - | - | +| data_filled | - | - | - | +| data_search | - | - | - | +| data_search_filled | - | - | - | +| delete | - | - | - | +| delete_1 | - | - | - | +| delete_1_filled | - | - | - | +| delete_filled | - | - | - | +| delete_time | - | - | - | +| delete_time_filled | - | - | - | +| delta | - | - | - | +| delta_filled | - | - | - | +| depressed | - | - | - | +| depressed_filled | - | - | - | +| desktop | - | - | - | +| desktop_1 | - | - | - | +| desktop_1_filled | - | - | - | +| desktop_filled | - | - | - | +| despise | - | - | - | +| despise_filled | - | - | - | +| device | - | - | - | +| device_filled | - | - | - | +| discount | - | - | - | +| discount_filled | - | - | - | +| dissatisfaction | - | - | - | +| dissatisfaction_filled | - | - | - | +| divide | - | - | - | +| dividers | - | - | - | +| dividers_1 | - | - | - | +| doge | - | - | - | +| doge_filled | - | - | - | +| double_storey | - | - | - | +| double_storey_filled | - | - | - | +| download | - | - | - | +| download_1 | - | - | - | +| download_2 | - | - | - | +| download_2_filled | - | - | - | +| downscale | - | - | - | +| drag_drop | - | - | - | +| drag_move | - | - | - | +| drink | - | - | - | +| drink_filled | - | - | - | +| drumstick | - | - | - | +| drumstick_filled | - | - | - | +| dv | - | - | - | +| dv_filled | - | - | - | +| dvd | - | - | - | +| dvd_filled | - | - | - | +| earphone | - | - | - | +| earphone_filled | - | - | - | +| earth | - | - | - | +| earth_filled | - | - | - | +| edit | - | - | - | +| edit_1 | - | - | - | +| edit_1_filled | - | - | - | +| edit_2 | - | - | - | +| edit_2_filled | - | - | - | +| edit_filled | - | - | - | +| edit_off | - | - | - | +| edit_off_filled | - | - | - | +| education | - | - | - | +| education_filled | - | - | - | +| eggplant | - | - | - | +| eggplant_filled | - | - | - | +| ellipsis | - | - | - | +| emo_emotional | - | - | - | +| emo_emotional_filled | - | - | - | +| enter | - | - | - | +| equal | - | - | - | +| error | - | - | - | +| error_circle | - | - | - | +| error_circle_filled | - | - | - | +| error_triangle | - | - | - | +| error_triangle_filled | - | - | - | +| excited | - | - | - | +| excited_1 | - | - | - | +| excited_1_filled | - | - | - | +| excited_filled | - | - | - | +| expand_down | - | - | - | +| expand_down_filled | - | - | - | +| expand_horizontal | - | - | - | +| expand_up | - | - | - | +| expand_up_filled | - | - | - | +| expand_vertical | - | - | - | +| explore | - | - | - | +| explore_filled | - | - | - | +| explore_off | - | - | - | +| explore_off_filled | - | - | - | +| exposure | - | - | - | +| exposure_filled | - | - | - | +| extension | - | - | - | +| extension_filled | - | - | - | +| extension_off | - | - | - | +| extension_off_filled | - | - | - | +| face_retouching | - | - | - | +| face_retouching_filled | - | - | - | +| fact_check | - | - | - | +| fact_check_filled | - | - | - | +| fahrenheit_scale | - | - | - | +| feel_at_ease | - | - | - | +| feel_at_ease_filled | - | - | - | +| ferocious | - | - | - | +| ferocious_filled | - | - | - | +| ferris_wheel | - | - | - | +| ferris_wheel_filled | - | - | - | +| file | - | - | - | +| file_1 | - | - | - | +| file_1_filled | - | - | - | +| file_add | - | - | - | +| file_add_1 | - | - | - | +| file_add_1_filled | - | - | - | +| file_add_filled | - | - | - | +| file_attachment | - | - | - | +| file_attachment_filled | - | - | - | +| file_blocked | - | - | - | +| file_blocked_filled | - | - | - | +| file_code | - | - | - | +| file_code_1 | - | - | - | +| file_code_1_filled | - | - | - | +| file_code_filled | - | - | - | +| file_copy | - | - | - | +| file_copy_filled | - | - | - | +| file_download | - | - | - | +| file_download_filled | - | - | - | +| file_excel | - | - | - | +| file_excel_filled | - | - | - | +| file_export | - | - | - | +| file_export_filled | - | - | - | +| file_filled | - | - | - | +| file_icon | - | - | - | +| file_icon_filled | - | - | - | +| file_image | - | - | - | +| file_image_filled | - | - | - | +| file_import | - | - | - | +| file_import_filled | - | - | - | +| file_locked | - | - | - | +| file_locked_filled | - | - | - | +| file_minus | - | - | - | +| file_minus_filled | - | - | - | +| file_music | - | - | - | +| file_music_filled | - | - | - | +| file_onenote | - | - | - | +| file_onenote_filled | - | - | - | +| file_outlook | - | - | - | +| file_outlook_filled | - | - | - | +| file_paste | - | - | - | +| file_paste_filled | - | - | - | +| file_pdf | - | - | - | +| file_pdf_filled | - | - | - | +| file_powerpoint | - | - | - | +| file_powerpoint_filled | - | - | - | +| file_restore | - | - | - | +| file_restore_filled | - | - | - | +| file_safety | - | - | - | +| file_safety_filled | - | - | - | +| file_search | - | - | - | +| file_search_filled | - | - | - | +| file_setting | - | - | - | +| file_setting_filled | - | - | - | +| file_teams | - | - | - | +| file_teams_filled | - | - | - | +| file_transmit | - | - | - | +| file_transmit_double | - | - | - | +| file_transmit_double_filled | - | - | - | +| file_transmit_filled | - | - | - | +| file_unknown | - | - | - | +| file_unknown_filled | - | - | - | +| file_unlocked | - | - | - | +| file_unlocked_filled | - | - | - | +| file_word | - | - | - | +| file_word_filled | - | - | - | +| file_zip | - | - | - | +| file_zip_filled | - | - | - | +| fill_color | - | - | - | +| fill_color_1 | - | - | - | +| fill_color_1_filled | - | - | - | +| fill_color_filled | - | - | - | +| film | - | - | - | +| film_1 | - | - | - | +| film_1_filled | - | - | - | +| film_filled | - | - | - | +| filter | - | - | - | +| filter_1 | - | - | - | +| filter_1_filled | - | - | - | +| filter_2 | - | - | - | +| filter_2_filled | - | - | - | +| filter_3 | - | - | - | +| filter_3_filled | - | - | - | +| filter_clear | - | - | - | +| filter_clear_filled | - | - | - | +| filter_filled | - | - | - | +| filter_off | - | - | - | +| filter_off_filled | - | - | - | +| filter_sort | - | - | - | +| filter_sort_filled | - | - | - | +| fingerprint | - | - | - | +| fingerprint_1 | - | - | - | +| fingerprint_2 | - | - | - | +| fingerprint_3 | - | - | - | +| fish | - | - | - | +| fish_filled | - | - | - | +| flag | - | - | - | +| flag_1 | - | - | - | +| flag_1_filled | - | - | - | +| flag_2 | - | - | - | +| flag_2_filled | - | - | - | +| flag_3 | - | - | - | +| flag_3_filled | - | - | - | +| flag_4 | - | - | - | +| flag_4_filled | - | - | - | +| flag_filled | - | - | - | +| flashlight | - | - | - | +| flashlight_filled | - | - | - | +| flight_landing | - | - | - | +| flight_landing_filled | - | - | - | +| flight_takeoff | - | - | - | +| flight_takeoff_filled | - | - | - | +| flip_smiling_face | - | - | - | +| flip_smiling_face_filled | - | - | - | +| flip_to_back | - | - | - | +| flip_to_back_filled | - | - | - | +| flip_to_front | - | - | - | +| flip_to_front_filled | - | - | - | +| focus | - | - | - | +| focus_filled | - | - | - | +| fog | - | - | - | +| fog_filled | - | - | - | +| fog_night | - | - | - | +| fog_night_filled | - | - | - | +| fog_sunny | - | - | - | +| fog_sunny_filled | - | - | - | +| folder | - | - | - | +| folder_1 | - | - | - | +| folder_1_filled | - | - | - | +| folder_add | - | - | - | +| folder_add_1 | - | - | - | +| folder_add_1_filled | - | - | - | +| folder_add_filled | - | - | - | +| folder_blocked | - | - | - | +| folder_blocked_filled | - | - | - | +| folder_details | - | - | - | +| folder_details_filled | - | - | - | +| folder_export | - | - | - | +| folder_export_filled | - | - | - | +| folder_filled | - | - | - | +| folder_import | - | - | - | +| folder_import_filled | - | - | - | +| folder_locked | - | - | - | +| folder_locked_filled | - | - | - | +| folder_minus | - | - | - | +| folder_minus_filled | - | - | - | +| folder_move | - | - | - | +| folder_move_filled | - | - | - | +| folder_off | - | - | - | +| folder_off_filled | - | - | - | +| folder_open | - | - | - | +| folder_open_1 | - | - | - | +| folder_open_1_filled | - | - | - | +| folder_open_filled | - | - | - | +| folder_search | - | - | - | +| folder_search_filled | - | - | - | +| folder_setting | - | - | - | +| folder_setting_filled | - | - | - | +| folder_shared | - | - | - | +| folder_shared_filled | - | - | - | +| folder_unlocked | - | - | - | +| folder_unlocked_filled | - | - | - | +| folder_zip | - | - | - | +| folder_zip_filled | - | - | - | +| forest | - | - | - | +| forest_filled | - | - | - | +| fork | - | - | - | +| fork_filled | - | - | - | +| form | - | - | - | +| form_filled | - | - | - | +| format_horizontal_align_bottom | - | - | - | +| format_horizontal_align_center | - | - | - | +| format_horizontal_align_top | - | - | - | +| format_vertical_align_center | - | - | - | +| format_vertical_align_left | - | - | - | +| format_vertical_align_right | - | - | - | +| forward | - | - | - | +| forward_filled | - | - | - | +| frame | - | - | - | +| frame_1 | - | - | - | +| frame_1_filled | - | - | - | +| frame_filled | - | - | - | +| fries | - | - | - | +| fries_filled | - | - | - | +| fullscreen | - | - | - | +| fullscreen_1 | - | - | - | +| fullscreen_2 | - | - | - | +| fullscreen_exit | - | - | - | +| fullscreen_exit_1 | - | - | - | +| function_curve | - | - | - | +| functions | - | - | - | +| functions_1 | - | - | - | +| gamepad | - | - | - | +| gamepad_1 | - | - | - | +| gamepad_1_filled | - | - | - | +| gamepad_filled | - | - | - | +| gamma | - | - | - | +| garlic | - | - | - | +| garlic_filled | - | - | - | +| gender_female | - | - | - | +| gender_male | - | - | - | +| gesture_applause | - | - | - | +| gesture_applause_filled | - | - | - | +| gesture_click | - | - | - | +| gesture_click_filled | - | - | - | +| gesture_down | - | - | - | +| gesture_down_filled | - | - | - | +| gesture_expansion | - | - | - | +| gesture_expansion_filled | - | - | - | +| gesture_left | - | - | - | +| gesture_left_filled | - | - | - | +| gesture_left_slip | - | - | - | +| gesture_left_slip_filled | - | - | - | +| gesture_open | - | - | - | +| gesture_open_filled | - | - | - | +| gesture_pray | - | - | - | +| gesture_pray_filled | - | - | - | +| gesture_press | - | - | - | +| gesture_press_filled | - | - | - | +| gesture_ranslation | - | - | - | +| gesture_ranslation_filled | - | - | - | +| gesture_right | - | - | - | +| gesture_right_filled | - | - | - | +| gesture_right_slip | - | - | - | +| gesture_right_slip_filled | - | - | - | +| gesture_slide_left_and_right | - | - | - | +| gesture_slide_left_and_right_filled | - | - | - | +| gesture_slide_up | - | - | - | +| gesture_slide_up_filled | - | - | - | +| gesture_typing | - | - | - | +| gesture_typing_filled | - | - | - | +| gesture_up | - | - | - | +| gesture_up_and_down | - | - | - | +| gesture_up_and_down_filled | - | - | - | +| gesture_up_filled | - | - | - | +| gesture_wipe_down | - | - | - | +| gesture_wipe_down_filled | - | - | - | +| gift | - | - | - | +| gift_filled | - | - | - | +| giggle | - | - | - | +| giggle_filled | - | - | - | +| git_branch | - | - | - | +| git_branch_filled | - | - | - | +| git_commit | - | - | - | +| git_commit_filled | - | - | - | +| git_merge | - | - | - | +| git_merge_filled | - | - | - | +| git_pull_request | - | - | - | +| git_pull_request_filled | - | - | - | +| git_repository | - | - | - | +| git_repository_commits | - | - | - | +| git_repository_commits_filled | - | - | - | +| git_repository_filled | - | - | - | +| git_repository_private | - | - | - | +| git_repository_private_filled | - | - | - | +| gps | - | - | - | +| gps_filled | - | - | - | +| grape | - | - | - | +| grape_filled | - | - | - | +| greater_than | - | - | - | +| greater_than_or_equal | - | - | - | +| green_onion | - | - | - | +| grid_add | - | - | - | +| grid_add_filled | - | - | - | +| grid_view | - | - | - | +| grid_view_filled | - | - | - | +| guitar | - | - | - | +| guitar_filled | - | - | - | +| hamburger | - | - | - | +| hamburger_filled | - | - | - | +| happy | - | - | - | +| happy_filled | - | - | - | +| hard_disk_storage | - | - | - | +| hard_disk_storage_filled | - | - | - | +| hard_drive | - | - | - | +| hard_drive_filled | - | - | - | +| hashtag | - | - | - | +| hd | - | - | - | +| hd_filled | - | - | - | +| heart | - | - | - | +| heart_filled | - | - | - | +| help | - | - | - | +| help_circle | - | - | - | +| help_circle_filled | - | - | - | +| help_rectangle | - | - | - | +| help_rectangle_filled | - | - | - | +| highlight | - | - | - | +| highlight_1 | - | - | - | +| highlight_1_filled | - | - | - | +| history | - | - | - | +| history_setting | - | - | - | +| home | - | - | - | +| home_filled | - | - | - | +| horizontal | - | - | - | +| horizontal_filled | - | - | - | +| hospital | - | - | - | +| hospital_1 | - | - | - | +| hospital_1_filled | - | - | - | +| hospital_filled | - | - | - | +| hotspot_wave | - | - | - | +| hotspot_wave_filled | - | - | - | +| hourglass | - | - | - | +| hourglass_filled | - | - | - | +| houses | - | - | - | +| houses_1 | - | - | - | +| houses_1_filled | - | - | - | +| houses_2 | - | - | - | +| houses_2_filled | - | - | - | +| houses_filled | - | - | - | +| html5 | - | - | - | +| html5_filled | - | - | - | +| https | - | - | - | +| https_filled | - | - | - | +| ice_cream | - | - | - | +| ice_cream_filled | - | - | - | +| icon | - | - | - | +| icon_filled | - | - | - | +| image | - | - | - | +| image_1 | - | - | - | +| image_1_filled | - | - | - | +| image_add | - | - | - | +| image_add_filled | - | - | - | +| image_edit | - | - | - | +| image_edit_filled | - | - | - | +| image_error | - | - | - | +| image_error_filled | - | - | - | +| image_filled | - | - | - | +| image_off | - | - | - | +| image_off_filled | - | - | - | +| image_search | - | - | - | +| image_search_filled | - | - | - | +| indent_left | - | - | - | +| indent_right | - | - | - | +| indicator | - | - | - | +| indicator_filled | - | - | - | +| info_circle | - | - | - | +| info_circle_filled | - | - | - | +| ink | - | - | - | +| ink_filled | - | - | - | +| install | - | - | - | +| install_desktop | - | - | - | +| install_desktop_filled | - | - | - | +| install_filled | - | - | - | +| install_mobile | - | - | - | +| install_mobile_filled | - | - | - | +| institution | - | - | - | +| institution_checked | - | - | - | +| institution_checked_filled | - | - | - | +| institution_filled | - | - | - | +| internet | - | - | - | +| internet_filled | - | - | - | +| ipod | - | - | - | +| ipod_filled | - | - | - | +| joyful | - | - | - | +| joyful_filled | - | - | - | +| jump | - | - | - | +| jump_double | - | - | - | +| jump_off | - | - | - | +| key | - | - | - | +| key_filled | - | - | - | +| keyboard | - | - | - | +| keyboard_filled | - | - | - | +| laptop | - | - | - | +| laptop_filled | - | - | - | +| layers | - | - | - | +| layers_filled | - | - | - | +| layout | - | - | - | +| layout_filled | - | - | - | +| leaderboard | - | - | - | +| leaderboard_filled | - | - | - | +| lemon | - | - | - | +| lemon_filled | - | - | - | +| lemon_slice | - | - | - | +| lemon_slice_filled | - | - | - | +| less_than | - | - | - | +| less_than_or_equal | - | - | - | +| letters_a | - | - | - | +| letters_b | - | - | - | +| letters_c | - | - | - | +| letters_d | - | - | - | +| letters_e | - | - | - | +| letters_f | - | - | - | +| letters_g | - | - | - | +| letters_h | - | - | - | +| letters_i | - | - | - | +| letters_j | - | - | - | +| letters_k | - | - | - | +| letters_l | - | - | - | +| letters_m | - | - | - | +| letters_n | - | - | - | +| letters_o | - | - | - | +| letters_p | - | - | - | +| letters_q | - | - | - | +| letters_r | - | - | - | +| letters_s | - | - | - | +| letters_t | - | - | - | +| letters_u | - | - | - | +| letters_v | - | - | - | +| letters_w | - | - | - | +| letters_x | - | - | - | +| letters_y | - | - | - | +| letters_z | - | - | - | +| lightbulb | - | - | - | +| lightbulb_circle | - | - | - | +| lightbulb_circle_filled | - | - | - | +| lightbulb_filled | - | - | - | +| lighthouse | - | - | - | +| lighthouse_1 | - | - | - | +| lighthouse_1_filled | - | - | - | +| lighthouse_2 | - | - | - | +| lighthouse_2_filled | - | - | - | +| lighthouse_filled | - | - | - | +| lighting_circle | - | - | - | +| lighting_circle_filled | - | - | - | +| line_height | - | - | - | +| link | - | - | - | +| link_1 | - | - | - | +| link_unlink | - | - | - | +| liquor | - | - | - | +| liquor_filled | - | - | - | +| list | - | - | - | +| list_numbered | - | - | - | +| load | - | - | - | +| loading | - | - | - | +| location | - | - | - | +| location_1 | - | - | - | +| location_1_filled | - | - | - | +| location_enlargement | - | - | - | +| location_enlargement_filled | - | - | - | +| location_error | - | - | - | +| location_error_filled | - | - | - | +| location_filled | - | - | - | +| location_parking_place | - | - | - | +| location_parking_place_filled | - | - | - | +| location_reduction | - | - | - | +| location_reduction_filled | - | - | - | +| location_setting | - | - | - | +| location_setting_filled | - | - | - | +| lock_off | - | - | - | +| lock_off_filled | - | - | - | +| lock_on | - | - | - | +| lock_on_filled | - | - | - | +| lock_time | - | - | - | +| lock_time_filled | - | - | - | +| login | - | - | - | +| logo_adobe_illustrate | - | - | - | +| logo_adobe_illustrate_filled | - | - | - | +| logo_adobe_lightroom | - | - | - | +| logo_adobe_lightroom_filled | - | - | - | +| logo_adobe_photoshop | - | - | - | +| logo_adobe_photoshop_filled | - | - | - | +| logo_android | - | - | - | +| logo_android_filled | - | - | - | +| logo_apple | - | - | - | +| logo_apple_filled | - | - | - | +| logo_behance | - | - | - | +| logo_chrome | - | - | - | +| logo_chrome_filled | - | - | - | +| logo_cinema4d | - | - | - | +| logo_cinema4d_filled | - | - | - | +| logo_codepen | - | - | - | +| logo_codesandbox | - | - | - | +| logo_dribbble | - | - | - | +| logo_dribbble_filled | - | - | - | +| logo_facebook | - | - | - | +| logo_facebook_filled | - | - | - | +| logo_figma | - | - | - | +| logo_figma_filled | - | - | - | +| logo_framer | - | - | - | +| logo_framer_filled | - | - | - | +| logo_github | - | - | - | +| logo_github_filled | - | - | - | +| logo_gitlab | - | - | - | +| logo_gitlab_filled | - | - | - | +| logo_ie | - | - | - | +| logo_ie_filled | - | - | - | +| logo_instagram | - | - | - | +| logo_instagram_filled | - | - | - | +| logo_qq | - | - | - | +| logo_qq_filled | - | - | - | +| logo_twitter | - | - | - | +| logo_twitter_filled | - | - | - | +| logo_wechat_stroke | - | - | - | +| logo_wechat_stroke_filled | - | - | - | +| logo_wechatpay | - | - | - | +| logo_wechatpay_filled | - | - | - | +| logo_wecom | - | - | - | +| logo_wecom_filled | - | - | - | +| logo_windows | - | - | - | +| logo_windows_filled | - | - | - | +| logo_youtube | - | - | - | +| logo_youtube_filled | - | - | - | +| logout | - | - | - | +| look_around | - | - | - | +| look_around_filled | - | - | - | +| loudspeaker | - | - | - | +| loudspeaker_filled | - | - | - | +| mail | - | - | - | +| mail_filled | - | - | - | +| map | - | - | - | +| map_3d | - | - | - | +| map_3d_filled | - | - | - | +| map_add | - | - | - | +| map_add_filled | - | - | - | +| map_aiming | - | - | - | +| map_aiming_filled | - | - | - | +| map_blocked | - | - | - | +| map_blocked_filled | - | - | - | +| map_bubble | - | - | - | +| map_bubble_filled | - | - | - | +| map_cancel | - | - | - | +| map_cancel_filled | - | - | - | +| map_chat | - | - | - | +| map_chat_filled | - | - | - | +| map_checked | - | - | - | +| map_checked_filled | - | - | - | +| map_collection | - | - | - | +| map_collection_filled | - | - | - | +| map_connection | - | - | - | +| map_connection_filled | - | - | - | +| map_distance | - | - | - | +| map_distance_filled | - | - | - | +| map_double | - | - | - | +| map_double_filled | - | - | - | +| map_edit | - | - | - | +| map_edit_filled | - | - | - | +| map_filled | - | - | - | +| map_grid | - | - | - | +| map_grid_filled | - | - | - | +| map_information | - | - | - | +| map_information_1 | - | - | - | +| map_information_1_filled | - | - | - | +| map_information_2 | - | - | - | +| map_information_2_filled | - | - | - | +| map_information_filled | - | - | - | +| map_location | - | - | - | +| map_location_filled | - | - | - | +| map_locked | - | - | - | +| map_locked_filled | - | - | - | +| map_marked | - | - | - | +| map_marked_filled | - | - | - | +| map_navigation | - | - | - | +| map_navigation_filled | - | - | - | +| map_outline | - | - | - | +| map_outline_filled | - | - | - | +| map_route_planning | - | - | - | +| map_route_planning_filled | - | - | - | +| map_ruler | - | - | - | +| map_ruler_filled | - | - | - | +| map_safety | - | - | - | +| map_safety_filled | - | - | - | +| map_search | - | - | - | +| map_search_1 | - | - | - | +| map_search_1_filled | - | - | - | +| map_search_filled | - | - | - | +| map_setting | - | - | - | +| map_setting_filled | - | - | - | +| map_unlocked | - | - | - | +| map_unlocked_filled | - | - | - | +| mark_as_unread | - | - | - | +| mark_as_unread_filled | - | - | - | +| markup | - | - | - | +| markup_filled | - | - | - | +| mathematics | - | - | - | +| mathematics_filled | - | - | - | +| measurement | - | - | - | +| measurement_1 | - | - | - | +| measurement_1_filled | - | - | - | +| measurement_2 | - | - | - | +| measurement_2_filled | - | - | - | +| measurement_filled | - | - | - | +| meat_pepper | - | - | - | +| meat_pepper_filled | - | - | - | +| media_library | - | - | - | +| media_library_filled | - | - | - | +| member | - | - | - | +| member_filled | - | - | - | +| menu | - | - | - | +| menu_application | - | - | - | +| menu_filled | - | - | - | +| menu_fold | - | - | - | +| menu_unfold | - | - | - | +| merge_cells | - | - | - | +| merge_cells_filled | - | - | - | +| microphone | - | - | - | +| microphone_1 | - | - | - | +| microphone_1_filled | - | - | - | +| microphone_2 | - | - | - | +| microphone_2_filled | - | - | - | +| microphone_filled | - | - | - | +| milk | - | - | - | +| milk_filled | - | - | - | +| minus | - | - | - | +| minus_circle | - | - | - | +| minus_circle_filled | - | - | - | +| minus_rectangle | - | - | - | +| minus_rectangle_filled | - | - | - | +| mirror | - | - | - | +| mirror_filled | - | - | - | +| mobile | - | - | - | +| mobile_blocked | - | - | - | +| mobile_blocked_filled | - | - | - | +| mobile_filled | - | - | - | +| mobile_list | - | - | - | +| mobile_list_filled | - | - | - | +| mobile_navigation | - | - | - | +| mobile_navigation_filled | - | - | - | +| mobile_shortcut | - | - | - | +| mobile_shortcut_filled | - | - | - | +| mobile_vibrate | - | - | - | +| mobile_vibrate_filled | - | - | - | +| mode_dark | - | - | - | +| mode_dark_filled | - | - | - | +| mode_light | - | - | - | +| mode_light_filled | - | - | - | +| module | - | - | - | +| module_filled | - | - | - | +| money | - | - | - | +| money_filled | - | - | - | +| monument | - | - | - | +| monument_filled | - | - | - | +| moon | - | - | - | +| moon_fall | - | - | - | +| moon_fall_filled | - | - | - | +| moon_filled | - | - | - | +| moon_rising | - | - | - | +| moon_rising_filled | - | - | - | +| more | - | - | - | +| mosque | - | - | - | +| mosque_1 | - | - | - | +| mosque_1_filled | - | - | - | +| mosque_filled | - | - | - | +| mouse | - | - | - | +| mouse_filled | - | - | - | +| move | - | - | - | +| move_1 | - | - | - | +| movie_clapper | - | - | - | +| movie_clapper_filled | - | - | - | +| multiply | - | - | - | +| museum | - | - | - | +| museum_1 | - | - | - | +| museum_1_filled | - | - | - | +| museum_2 | - | - | - | +| museum_2_filled | - | - | - | +| museum_filled | - | - | - | +| mushroom | - | - | - | +| mushroom_1 | - | - | - | +| mushroom_1_filled | - | - | - | +| mushroom_filled | - | - | - | +| music | - | - | - | +| music_1 | - | - | - | +| music_1_filled | - | - | - | +| music_2 | - | - | - | +| music_2_filled | - | - | - | +| music_filled | - | - | - | +| music_rectangle_add | - | - | - | +| music_rectangle_add_filled | - | - | - | +| navigation_arrow | - | - | - | +| navigation_arrow_filled | - | - | - | +| next | - | - | - | +| next_filled | - | - | - | +| no_expression | - | - | - | +| no_expression_filled | - | - | - | +| noodle | - | - | - | +| noodle_filled | - | - | - | +| notification | - | - | - | +| notification_add | - | - | - | +| notification_add_filled | - | - | - | +| notification_circle | - | - | - | +| notification_circle_filled | - | - | - | +| notification_error | - | - | - | +| notification_error_filled | - | - | - | +| notification_filled | - | - | - | +| numbers_0 | - | - | - | +| numbers_0_1 | - | - | - | +| numbers_1 | - | - | - | +| numbers_1_1 | - | - | - | +| numbers_2 | - | - | - | +| numbers_2_1 | - | - | - | +| numbers_3 | - | - | - | +| numbers_3_1 | - | - | - | +| numbers_4 | - | - | - | +| numbers_4_1 | - | - | - | +| numbers_5 | - | - | - | +| numbers_5_1 | - | - | - | +| numbers_6 | - | - | - | +| numbers_6_1 | - | - | - | +| numbers_7 | - | - | - | +| numbers_7_1 | - | - | - | +| numbers_8 | - | - | - | +| numbers_8_1 | - | - | - | +| numbers_9 | - | - | - | +| numbers_9_1 | - | - | - | +| nut | - | - | - | +| nut_filled | - | - | - | +| object_storage | - | - | - | +| open_mouth | - | - | - | +| open_mouth_filled | - | - | - | +| opera | - | - | - | +| opera_filled | - | - | - | +| order_adjustment_column | - | - | - | +| order_ascending | - | - | - | +| order_descending | - | - | - | +| outbox | - | - | - | +| outbox_filled | - | - | - | +| page_first | - | - | - | +| page_head | - | - | - | +| page_head_filled | - | - | - | +| page_last | - | - | - | +| palace | - | - | - | +| palace_1 | - | - | - | +| palace_1_filled | - | - | - | +| palace_2 | - | - | - | +| palace_2_filled | - | - | - | +| palace_3 | - | - | - | +| palace_3_filled | - | - | - | +| palace_4 | - | - | - | +| palace_4_filled | - | - | - | +| palace_filled | - | - | - | +| palette | - | - | - | +| palette_1 | - | - | - | +| palette_1_filled | - | - | - | +| palette_filled | - | - | - | +| panorama_horizontal | - | - | - | +| panorama_horizontal_filled | - | - | - | +| panorama_vertical | - | - | - | +| panorama_vertical_filled | - | - | - | +| pantone | - | - | - | +| pantone_filled | - | - | - | +| parabola | - | - | - | +| parentheses | - | - | - | +| paste | - | - | - | +| paste_filled | - | - | - | +| patio | - | - | - | +| patio_filled | - | - | - | +| pause | - | - | - | +| pause_circle | - | - | - | +| pause_circle_filled | - | - | - | +| pause_circle_stroke | - | - | - | +| pause_circle_stroke_filled | - | - | - | +| pea | - | - | - | +| pea_filled | - | - | - | +| peach | - | - | - | +| peach_filled | - | - | - | +| pear | - | - | - | +| pear_filled | - | - | - | +| pearl_of_the_orient | - | - | - | +| pearl_of_the_orient_filled | - | - | - | +| pen | - | - | - | +| pen_ball | - | - | - | +| pen_ball_filled | - | - | - | +| pen_brush | - | - | - | +| pen_brush_filled | - | - | - | +| pen_filled | - | - | - | +| pen_mark | - | - | - | +| pen_mark_filled | - | - | - | +| pen_quill | - | - | - | +| pen_quill_filled | - | - | - | +| pending | - | - | - | +| pending_filled | - | - | - | +| percent | - | - | - | +| personal_information | - | - | - | +| personal_information_filled | - | - | - | +| phone_locked | - | - | - | +| phone_locked_filled | - | - | - | +| phone_search | - | - | - | +| phone_search_filled | - | - | - | +| pi | - | - | - | +| piano | - | - | - | +| piano_filled | - | - | - | +| pin | - | - | - | +| pin_filled | - | - | - | +| play | - | - | - | +| play_circle | - | - | - | +| play_circle_filled | - | - | - | +| play_circle_stroke | - | - | - | +| play_circle_stroke_add | - | - | - | +| play_circle_stroke_add_filled | - | - | - | +| play_circle_stroke_filled | - | - | - | +| play_demo | - | - | - | +| play_demo_filled | - | - | - | +| play_rectangle | - | - | - | +| play_rectangle_filled | - | - | - | +| plus | - | - | - | +| popsicle | - | - | - | +| popsicle_filled | - | - | - | +| portrait | - | - | - | +| portrait_filled | - | - | - | +| pout | - | - | - | +| pout_filled | - | - | - | +| poweroff | - | - | - | +| precise_monitor | - | - | - | +| previous | - | - | - | +| previous_filled | - | - | - | +| print | - | - | - | +| print_filled | - | - | - | +| pumpkin | - | - | - | +| pumpkin_filled | - | - | - | +| pyramid | - | - | - | +| pyramid_filled | - | - | - | +| pyramid_maya | - | - | - | +| pyramid_maya_filled | - | - | - | +| qrcode | - | - | - | +| quadratic | - | - | - | +| questionnaire | - | - | - | +| questionnaire_double | - | - | - | +| questionnaire_double_filled | - | - | - | +| questionnaire_filled | - | - | - | +| queue | - | - | - | +| queue_filled | - | - | - | +| radar | - | - | - | +| radio_1 | - | - | - | +| radio_1_filled | - | - | - | +| radio_2 | - | - | - | +| radio_2_filled | - | - | - | +| radish | - | - | - | +| radish_filled | - | - | - | +| rain_heavy | - | - | - | +| rain_light | - | - | - | +| rain_light_filled | - | - | - | +| rain_medium | - | - | - | +| rainbow | - | - | - | +| rectangle | - | - | - | +| rectangle_filled | - | - | - | +| refresh | - | - | - | +| relation | - | - | - | +| relativity | - | - | - | +| relativity_filled | - | - | - | +| remote_wave | - | - | - | +| remote_wave_filled | - | - | - | +| remove | - | - | - | +| replay | - | - | - | +| replay_filled | - | - | - | +| rice | - | - | - | +| rice_ball | - | - | - | +| rice_ball_filled | - | - | - | +| rice_filled | - | - | - | +| roast | - | - | - | +| roast_filled | - | - | - | +| rocket | - | - | - | +| rocket_filled | - | - | - | +| rollback | - | - | - | +| rollfront | - | - | - | +| root_list | - | - | - | +| root_list_filled | - | - | - | +| rotate | - | - | - | +| rotate_locked | - | - | - | +| rotate_locked_filled | - | - | - | +| rotation | - | - | - | +| round | - | - | - | +| round_filled | - | - | - | +| router_wave | - | - | - | +| router_wave_filled | - | - | - | +| rss | - | - | - | +| ruler | - | - | - | +| ruler_filled | - | - | - | +| sailing_hotel | - | - | - | +| sailing_hotel_filled | - | - | - | +| sandwich | - | - | - | +| sandwich_filled | - | - | - | +| saturation | - | - | - | +| saturation_filled | - | - | - | +| sausage | - | - | - | +| sausage_filled | - | - | - | +| save | - | - | - | +| save_filled | - | - | - | +| saving_pot | - | - | - | +| saving_pot_filled | - | - | - | +| scan | - | - | - | +| screen_4k | - | - | - | +| screen_4k_filled | - | - | - | +| screencast | - | - | - | +| screencast_filled | - | - | - | +| screenshot | - | - | - | +| scroll_bar | - | - | - | +| scroll_bar_filled | - | - | - | +| sd_card | - | - | - | +| sd_card_1 | - | - | - | +| sd_card_1_filled | - | - | - | +| sd_card_filled | - | - | - | +| search | - | - | - | +| search_error | - | - | - | +| search_error_filled | - | - | - | +| search_filled | - | - | - | +| secured | - | - | - | +| secured_filled | - | - | - | +| send | - | - | - | +| send_cancel | - | - | - | +| send_cancel_filled | - | - | - | +| send_filled | - | - | - | +| sensors | - | - | - | +| sensors_1 | - | - | - | +| sensors_2 | - | - | - | +| sensors_off | - | - | - | +| sequence | - | - | - | +| sequence_filled | - | - | - | +| serenity | - | - | - | +| serenity_filled | - | - | - | +| server | - | - | - | +| server_filled | - | - | - | +| service | - | - | - | +| service_filled | - | - | - | +| setting | - | - | - | +| setting_1 | - | - | - | +| setting_1_filled | - | - | - | +| setting_filled | - | - | - | +| share | - | - | - | +| share_1 | - | - | - | +| share_1_filled | - | - | - | +| share_filled | - | - | - | +| sharpness | - | - | - | +| sharpness_filled | - | - | - | +| shield_error | - | - | - | +| shield_error_filled | - | - | - | +| shimen | - | - | - | +| shimen_filled | - | - | - | +| shop | - | - | - | +| shop_1 | - | - | - | +| shop_1_filled | - | - | - | +| shop_2 | - | - | - | +| shop_2_filled | - | - | - | +| shop_3 | - | - | - | +| shop_3_filled | - | - | - | +| shop_4 | - | - | - | +| shop_4_filled | - | - | - | +| shop_5 | - | - | - | +| shop_5_filled | - | - | - | +| shop_filled | - | - | - | +| shrimp | - | - | - | +| shrimp_filled | - | - | - | +| shrink_horizontal | - | - | - | +| shrink_vertical | - | - | - | +| shutter | - | - | - | +| shutter_filled | - | - | - | +| shutup | - | - | - | +| shutup_filled | - | - | - | +| sim_card | - | - | - | +| sim_card_1 | - | - | - | +| sim_card_1_filled | - | - | - | +| sim_card_2 | - | - | - | +| sim_card_2_filled | - | - | - | +| sim_card_filled | - | - | - | +| sinister_smile | - | - | - | +| sinister_smile_filled | - | - | - | +| sip | - | - | - | +| sip_filled | - | - | - | +| sitemap | - | - | - | +| sitemap_filled | - | - | - | +| slash | - | - | - | +| sleep | - | - | - | +| sleep_filled | - | - | - | +| slice | - | - | - | +| slice_filled | - | - | - | +| slideshow | - | - | - | +| slideshow_filled | - | - | - | +| smile | - | - | - | +| smile_filled | - | - | - | +| sneer | - | - | - | +| sneer_filled | - | - | - | +| snowflake | - | - | - | +| sonic | - | - | - | +| sound | - | - | - | +| sound_down | - | - | - | +| sound_down_filled | - | - | - | +| sound_filled | - | - | - | +| sound_high | - | - | - | +| sound_high_filled | - | - | - | +| sound_low | - | - | - | +| sound_low_filled | - | - | - | +| sound_mute | - | - | - | +| sound_mute_1 | - | - | - | +| sound_mute_1_filled | - | - | - | +| sound_mute_filled | - | - | - | +| sound_up | - | - | - | +| sound_up_filled | - | - | - | +| space | - | - | - | +| speechless | - | - | - | +| speechless_1 | - | - | - | +| speechless_1_filled | - | - | - | +| speechless_filled | - | - | - | +| star | - | - | - | +| star_filled | - | - | - | +| statue_of_jesus | - | - | - | +| statue_of_jesus_filled | - | - | - | +| sticky_note | - | - | - | +| sticky_note_filled | - | - | - | +| stop | - | - | - | +| stop_circle | - | - | - | +| stop_circle_filled | - | - | - | +| stop_circle_stroke | - | - | - | +| stop_circle_stroke_filled | - | - | - | +| store | - | - | - | +| store_filled | - | - | - | +| street_road | - | - | - | +| street_road_1 | - | - | - | +| street_road_1_filled | - | - | - | +| street_road_filled | - | - | - | +| subtitle | - | - | - | +| subtitle_filled | - | - | - | +| subway_line | - | - | - | +| subway_line_filled | - | - | - | +| sum | - | - | - | +| sun_fall | - | - | - | +| sun_fall_filled | - | - | - | +| sun_rising | - | - | - | +| sun_rising_filled | - | - | - | +| sunny | - | - | - | +| sunny_filled | - | - | - | +| support | - | - | - | +| support_filled | - | - | - | +| surprised | - | - | - | +| surprised_1 | - | - | - | +| surprised_1_filled | - | - | - | +| surprised_filled | - | - | - | +| swap | - | - | - | +| swap_left | - | - | - | +| swap_right | - | - | - | +| swear_1 | - | - | - | +| swear_1_filled | - | - | - | +| swear_2 | - | - | - | +| swear_2_filled | - | - | - | +| system_2 | - | - | - | +| system_3 | - | - | - | +| system_3_filled | - | - | - | +| system_application | - | - | - | +| system_application_filled | - | - | - | +| system_blocked | - | - | - | +| system_blocked_filled | - | - | - | +| system_code | - | - | - | +| system_code_filled | - | - | - | +| system_components | - | - | - | +| system_components_filled | - | - | - | +| system_coordinate | - | - | - | +| system_coordinate_filled | - | - | - | +| system_device | - | - | - | +| system_device_filled | - | - | - | +| system_interface | - | - | - | +| system_interface_filled | - | - | - | +| system_location | - | - | - | +| system_location_filled | - | - | - | +| system_locked | - | - | - | +| system_locked_filled | - | - | - | +| system_log | - | - | - | +| system_log_filled | - | - | - | +| system_marked | - | - | - | +| system_marked_filled | - | - | - | +| system_messages | - | - | - | +| system_messages_filled | - | - | - | +| system_regulation | - | - | - | +| system_regulation_filled | - | - | - | +| system_search | - | - | - | +| system_search_filled | - | - | - | +| system_setting | - | - | - | +| system_setting_filled | - | - | - | +| system_storage | - | - | - | +| system_storage_filled | - | - | - | +| system_sum | - | - | - | +| system_unlocked | - | - | - | +| system_unlocked_filled | - | - | - | +| tab | - | - | - | +| tab_filled | - | - | - | +| table | - | - | - | +| table_1 | - | - | - | +| table_1_filled | - | - | - | +| table_2 | - | - | - | +| table_2_filled | - | - | - | +| table_add | - | - | - | +| table_add_filled | - | - | - | +| table_filled | - | - | - | +| table_split | - | - | - | +| table_split_filled | - | - | - | +| tag | - | - | - | +| tag_filled | - | - | - | +| tangerinr | - | - | - | +| tangerinr_filled | - | - | - | +| tape | - | - | - | +| tape_filled | - | - | - | +| task | - | - | - | +| task_1 | - | - | - | +| task_1_filled | - | - | - | +| task_add | - | - | - | +| task_add_1 | - | - | - | +| task_add_filled | - | - | - | +| task_checked | - | - | - | +| task_checked_1 | - | - | - | +| task_checked_filled | - | - | - | +| task_double | - | - | - | +| task_double_filled | - | - | - | +| task_error | - | - | - | +| task_error_filled | - | - | - | +| task_filled | - | - | - | +| task_location | - | - | - | +| task_location_filled | - | - | - | +| task_marked | - | - | - | +| task_marked_filled | - | - | - | +| task_setting | - | - | - | +| task_setting_filled | - | - | - | +| task_time | - | - | - | +| task_time_filled | - | - | - | +| task_visible | - | - | - | +| task_visible_filled | - | - | - | +| tea | - | - | - | +| tea_filled | - | - | - | +| teahouse | - | - | - | +| teahouse_filled | - | - | - | +| template | - | - | - | +| template_filled | - | - | - | +| temple | - | - | - | +| temple_filled | - | - | - | +| terminal | - | - | - | +| terminal_rectangle | - | - | - | +| terminal_rectangle_1 | - | - | - | +| terminal_rectangle_1_filled | - | - | - | +| terminal_rectangle_filled | - | - | - | +| terminal_window | - | - | - | +| terminal_window_filled | - | - | - | +| textbox | - | - | - | +| textbox_filled | - | - | - | +| textformat_bold | - | - | - | +| textformat_color | - | - | - | +| textformat_italic | - | - | - | +| textformat_strikethrough | - | - | - | +| textformat_underline | - | - | - | +| textformat_wrap | - | - | - | +| theaters | - | - | - | +| theaters_filled | - | - | - | +| thumb_down | - | - | - | +| thumb_down_1 | - | - | - | +| thumb_down_1_filled | - | - | - | +| thumb_down_2 | - | - | - | +| thumb_down_2_filled | - | - | - | +| thumb_down_filled | - | - | - | +| thumb_up | - | - | - | +| thumb_up_1 | - | - | - | +| thumb_up_1_filled | - | - | - | +| thumb_up_2 | - | - | - | +| thumb_up_2_filled | - | - | - | +| thumb_up_filled | - | - | - | +| thunder | - | - | - | +| thunderstorm | - | - | - | +| thunderstorm_night | - | - | - | +| thunderstorm_night_filled | - | - | - | +| thunderstorm_sunny | - | - | - | +| thunderstorm_sunny_filled | - | - | - | +| ticket | - | - | - | +| ticket_filled | - | - | - | +| time | - | - | - | +| time_filled | - | - | - | +| tips | - | - | - | +| tips_double | - | - | - | +| tips_double_filled | - | - | - | +| tips_filled | - | - | - | +| tomato | - | - | - | +| tomato_filled | - | - | - | +| tools | - | - | - | +| tools_circle | - | - | - | +| tools_circle_filled | - | - | - | +| tools_filled | - | - | - | +| tornado | - | - | - | +| tower | - | - | - | +| tower_1 | - | - | - | +| tower_1_filled | - | - | - | +| tower_2 | - | - | - | +| tower_2_filled | - | - | - | +| tower_3 | - | - | - | +| tower_3_filled | - | - | - | +| tower_clock | - | - | - | +| tower_clock_filled | - | - | - | +| tower_filled | - | - | - | +| town | - | - | - | +| town_filled | - | - | - | +| traffic | - | - | - | +| traffic_events | - | - | - | +| traffic_events_filled | - | - | - | +| traffic_filled | - | - | - | +| transform | - | - | - | +| transform_1 | - | - | - | +| transform_1_filled | - | - | - | +| transform_2 | - | - | - | +| transform_3 | - | - | - | +| transform_filled | - | - | - | +| translate | - | - | - | +| translate_1 | - | - | - | +| tree_round_dot | - | - | - | +| tree_round_dot_filled | - | - | - | +| tree_round_dot_vertical | - | - | - | +| tree_round_dot_vertical_filled | - | - | - | +| tree_square_dot | - | - | - | +| tree_square_dot_filled | - | - | - | +| tree_square_dot_vertical | - | - | - | +| tree_square_dot_vertical_filled | - | - | - | +| trending_down | - | - | - | +| trending_up | - | - | - | +| tv | - | - | - | +| tv_1 | - | - | - | +| tv_1_filled | - | - | - | +| tv_2 | - | - | - | +| tv_2_filled | - | - | - | +| tv_filled | - | - | - | +| typography | - | - | - | +| typography_filled | - | - | - | +| uncomfortable | - | - | - | +| uncomfortable_1 | - | - | - | +| uncomfortable_1_filled | - | - | - | +| uncomfortable_2 | - | - | - | +| uncomfortable_2_filled | - | - | - | +| uncomfortable_filled | - | - | - | +| undertake | - | - | - | +| undertake_delivery | - | - | - | +| undertake_delivery_filled | - | - | - | +| undertake_environment_protection | - | - | - | +| undertake_environment_protection_filled | - | - | - | +| undertake_filled | - | - | - | +| undertake_hold_up | - | - | - | +| undertake_hold_up_filled | - | - | - | +| undertake_transaction | - | - | - | +| undertake_transaction_filled | - | - | - | +| unfold_less | - | - | - | +| unfold_more | - | - | - | +| unhappy | - | - | - | +| unhappy_1 | - | - | - | +| unhappy_1_filled | - | - | - | +| unhappy_filled | - | - | - | +| uninstall | - | - | - | +| uninstall_filled | - | - | - | +| upload | - | - | - | +| upload_1 | - | - | - | +| upscale | - | - | - | +| usb | - | - | - | +| usb_filled | - | - | - | +| user | - | - | - | +| user_1 | - | - | - | +| user_1_filled | - | - | - | +| user_add | - | - | - | +| user_add_filled | - | - | - | +| user_arrow_down | - | - | - | +| user_arrow_down_filled | - | - | - | +| user_arrow_left | - | - | - | +| user_arrow_left_filled | - | - | - | +| user_arrow_right | - | - | - | +| user_arrow_right_filled | - | - | - | +| user_arrow_up | - | - | - | +| user_arrow_up_filled | - | - | - | +| user_avatar | - | - | - | +| user_avatar_filled | - | - | - | +| user_blocked | - | - | - | +| user_blocked_filled | - | - | - | +| user_business | - | - | - | +| user_business_filled | - | - | - | +| user_checked | - | - | - | +| user_checked_1 | - | - | - | +| user_checked_1_filled | - | - | - | +| user_checked_filled | - | - | - | +| user_circle | - | - | - | +| user_circle_filled | - | - | - | +| user_clear | - | - | - | +| user_clear_filled | - | - | - | +| user_error_1 | - | - | - | +| user_error_1_filled | - | - | - | +| user_filled | - | - | - | +| user_invisible | - | - | - | +| user_invisible_filled | - | - | - | +| user_list | - | - | - | +| user_list_filled | - | - | - | +| user_locked | - | - | - | +| user_locked_filled | - | - | - | +| user_marked | - | - | - | +| user_marked_filled | - | - | - | +| user_password | - | - | - | +| user_password_filled | - | - | - | +| user_safety | - | - | - | +| user_safety_filled | - | - | - | +| user_search | - | - | - | +| user_search_filled | - | - | - | +| user_setting | - | - | - | +| user_setting_filled | - | - | - | +| user_talk | - | - | - | +| user_talk_1 | - | - | - | +| user_talk_1_filled | - | - | - | +| user_talk_filled | - | - | - | +| user_talk_off_1 | - | - | - | +| user_talk_off_1_filled | - | - | - | +| user_time | - | - | - | +| user_time_filled | - | - | - | +| user_transmit | - | - | - | +| user_transmit_filled | - | - | - | +| user_unknown | - | - | - | +| user_unknown_filled | - | - | - | +| user_unlocked | - | - | - | +| user_unlocked_filled | - | - | - | +| user_vip | - | - | - | +| user_vip_filled | - | - | - | +| user_visible | - | - | - | +| user_visible_filled | - | - | - | +| usercase | - | - | - | +| usercase_filled | - | - | - | +| usercase_link | - | - | - | +| usercase_link_filled | - | - | - | +| usergroup | - | - | - | +| usergroup_add | - | - | - | +| usergroup_add_filled | - | - | - | +| usergroup_clear | - | - | - | +| usergroup_clear_filled | - | - | - | +| usergroup_filled | - | - | - | +| vehicle | - | - | - | +| vehicle_filled | - | - | - | +| verified | - | - | - | +| verified_filled | - | - | - | +| verify | - | - | - | +| verify_filled | - | - | - | +| vertical | - | - | - | +| vertical_filled | - | - | - | +| video | - | - | - | +| video_camera | - | - | - | +| video_camera_1 | - | - | - | +| video_camera_1_filled | - | - | - | +| video_camera_2 | - | - | - | +| video_camera_2_filled | - | - | - | +| video_camera_3 | - | - | - | +| video_camera_3_filled | - | - | - | +| video_camera_dollar | - | - | - | +| video_camera_dollar_filled | - | - | - | +| video_camera_filled | - | - | - | +| video_camera_minus | - | - | - | +| video_camera_minus_filled | - | - | - | +| video_camera_music | - | - | - | +| video_camera_music_filled | - | - | - | +| video_camera_off | - | - | - | +| video_camera_off_filled | - | - | - | +| video_filled | - | - | - | +| video_library | - | - | - | +| video_library_filled | - | - | - | +| view_agenda | - | - | - | +| view_agenda_filled | - | - | - | +| view_column | - | - | - | +| view_in_ar | - | - | - | +| view_in_ar_filled | - | - | - | +| view_list | - | - | - | +| view_module | - | - | - | +| view_module_filled | - | - | - | +| visual_recognition | - | - | - | +| visual_recognition_filled | - | - | - | +| wallet | - | - | - | +| wallet_filled | - | - | - | +| watch | - | - | - | +| watch_filled | - | - | - | +| watermelon | - | - | - | +| watermelon_filled | - | - | - | +| wave_bye | - | - | - | +| wave_bye_filled | - | - | - | +| wave_left | - | - | - | +| wave_left_filled | - | - | - | +| wave_right | - | - | - | +| wave_right_filled | - | - | - | +| wealth | - | - | - | +| wealth_1 | - | - | - | +| wealth_1_filled | - | - | - | +| wealth_filled | - | - | - | +| widget | - | - | - | +| widget_filled | - | - | - | +| wifi | - | - | - | +| wifi_1 | - | - | - | +| wifi_1_filled | - | - | - | +| wifi_off | - | - | - | +| wifi_off_1 | - | - | - | +| wifi_off_1_filled | - | - | - | +| window | - | - | - | +| window_1 | - | - | - | +| window_1_filled | - | - | - | +| window_filled | - | - | - | +| windy | - | - | - | +| windy_rain | - | - | - | +| wink | - | - | - | +| wink_filled | - | - | - | +| work | - | - | - | +| work_filled | - | - | - | +| work_history | - | - | - | +| work_history_filled | - | - | - | +| work_off | - | - | - | +| work_off_filled | - | - | - | +| wry_smile | - | - | - | +| wry_smile_filled | - | - | - | +| zoom_in | - | - | - | +| zoom_in_filled | - | - | - | +| zoom_out | - | - | - | +| zoom_out_filled | - | - | - | + #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TIcons._ | 私有构造方法,不支持外部创建,仅提供静态常量给外部使用 | +##### TIcons._ + +私有构造方法,不支持外部创建,仅提供静态常量给外部使用 \ No newline at end of file diff --git a/tdesign-component/example/assets/api/image-viewer_api.md b/tdesign-component/example/assets/api/image-viewer_api.md index f36abefb8..1cc94ffd9 100644 --- a/tdesign-component/example/assets/api/image-viewer_api.md +++ b/tdesign-component/example/assets/api/image-viewer_api.md @@ -3,12 +3,42 @@ #### 静态方法 -| 名称 | 返回类型 | 参数 | 说明 | +##### TImageViewer.showImageViewer + +显示图片预览 + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| showImageViewer | | required BuildContext context, required List images, List? labels, bool? closeBtn, bool? deleteBtn, bool? showIndex, bool? loop, bool? autoplay, int? duration, Color? bgColor, Color? navBarBgColor, Color? iconColor, TextStyle? labelStyle, TextStyle? indexStyle, Color? modalBarrierColor, bool? barrierDismissible, int? defaultIndex, double? width, double? height, OnIndexChange? onIndexChange, OnClose? onClose, OnDelete? onDelete, bool? ignoreDeleteError, OnImageTap? onTap, OnLongPress? onLongPress, LeftItemBuilder? leftItemBuilder, RightItemBuilder? rightItemBuilder, | 显示图片预览 | +| context | BuildContext | - | - | +| images | List | - | 图片数组 | +| labels | List? | - | 图片描述 | +| closeBtn | bool? | true | 是否展示关闭按钮 | +| deleteBtn | bool? | false | 是否显示删除操作 | +| showIndex | bool? | false | 是否显示页码 | +| loop | bool? | false | 图片是否循环 | +| autoplay | bool? | false | 图片轮播是否自动播放 | +| duration | int? | - | 自动播放间隔 | +| bgColor | Color? | - | 背景色 | +| navBarBgColor | Color? | - | 导航栏背景色 | +| iconColor | Color? | - | 图标颜色 | +| labelStyle | TextStyle? | - | label文字样式 | +| indexStyle | TextStyle? | - | 页码样式 | +| modalBarrierColor | Color? | - | - | +| barrierDismissible | bool? | - | - | +| defaultIndex | int? | - | 默认预览图片所在的下标 | +| width | double? | - | 图片宽度 | +| height | double? | - | 图片高度 | +| onIndexChange | OnIndexChange? | - | 预览图片切换回调 | +| onClose | OnClose? | - | 关闭点击 | +| onDelete | OnDelete? | - | 删除点击 | +| ignoreDeleteError | bool? | - | 是否忽略单张图片删除错误提示 | +| onTap | OnImageTap? | - | 点击图片 | +| onLongPress | OnLongPress? | - | 长按图片 | +| leftItemBuilder | LeftItemBuilder? | - | 左侧自定义操作 | +| rightItemBuilder | RightItemBuilder? | - | 右侧自定义操作 | -``` -``` ### TImageViewerWidget #### 默认构造方法 @@ -26,7 +56,7 @@ | ignoreDeleteError | bool? | false | 是否忽略单张图片删除错误提示 | | images | List | - | 图片数组 | | indexStyle | TextStyle? | - | 页码样式 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | labels | List? | - | 图片描述 | | labelStyle | TextStyle? | - | label文字样式 | | leftItemBuilder | LeftItemBuilder? | - | 左侧自定义操作 | @@ -40,3 +70,59 @@ | rightItemBuilder | RightItemBuilder? | - | 右侧自定义操作 | | showIndex | bool? | - | 是否显示页码 | | width | double? | - | 图片宽度 | + + +### OnIndexChange +#### 类型定义 + +```dart +typedef OnIndexChange = Function(int index); +``` + + +### OnClose +#### 类型定义 + +```dart +typedef OnClose = Function(int index); +``` + + +### OnDelete +#### 类型定义 + +```dart +typedef OnDelete = Function(int index); +``` + + +### OnImageTap +#### 类型定义 + +```dart +typedef OnImageTap = Function(int index); +``` + + +### OnLongPress +#### 类型定义 + +```dart +typedef OnLongPress = Function(int index); +``` + + +### LeftItemBuilder +#### 类型定义 + +```dart +typedef LeftItemBuilder = Widget Function(BuildContext context, int index); +``` + + +### RightItemBuilder +#### 类型定义 + +```dart +typedef RightItemBuilder = Widget Function(BuildContext context, int index); +``` diff --git a/tdesign-component/example/assets/api/image_api.md b/tdesign-component/example/assets/api/image_api.md index 133ef6adf..f49591872 100644 --- a/tdesign-component/example/assets/api/image_api.md +++ b/tdesign-component/example/assets/api/image_api.md @@ -4,30 +4,45 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| alignment | | Alignment.center | | +| alignment | AlignmentGeometry | Alignment.center | - | | assetUrl | String? | - | 本地素材地址 | -| cacheHeight | | - | | -| cacheWidth | | - | | -| centerSlice | | - | | -| color | | - | | -| colorBlendMode | | - | | -| errorBuilder | | - | | +| cacheHeight | int? | - | - | +| cacheWidth | int? | - | - | +| centerSlice | Rect? | - | - | +| color | Color? | - | - | +| colorBlendMode | BlendMode? | - | - | +| errorBuilder | ImageErrorWidgetBuilder? | - | - | | errorWidget | Widget? | - | 失败自定义提示 | -| excludeFromSemantics | | false | | -| filterQuality | | FilterQuality.low | | +| excludeFromSemantics | bool | false | - | +| filterQuality | FilterQuality | FilterQuality.low | - | | fit | BoxFit? | - | 适配样式 | | frameBuilder | ImageFrameBuilder? | - | 以下系统Image属性,释义请参考系统[Image]中注释 | -| gaplessPlayback | | false | | +| gaplessPlayback | bool | false | - | | height | double? | - | 自定义高 | | imageFile | File? | - | 图片文件路径 | | imgUrl | String? | - | 图片地址 | -| isAntiAlias | | false | | -| key | | - | | -| loadingBuilder | | - | | +| isAntiAlias | bool | false | - | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| loadingBuilder | ImageLoadingBuilder? | - | - | | loadingWidget | Widget? | - | 加载自定义提示 | -| matchTextDirection | | false | | -| opacity | | - | | -| repeat | | ImageRepeat.noRepeat | | -| semanticLabel | | - | | +| matchTextDirection | bool | false | - | +| opacity | Animation? | - | - | +| repeat | ImageRepeat | ImageRepeat.noRepeat | - | +| semanticLabel | String? | - | - | | type | TImageType | TImageType.roundedSquare | 图片类型 | | width | double? | - | 自定义宽 | + + +### TImageType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| clip | 裁剪 | +| fitHeight | 适应高 | +| fitWidth | 适应宽 | +| stretch | 拉伸 | +| square | 方形, | +| roundedSquare | 圆角方形 | +| circle | 圆形 | diff --git a/tdesign-component/example/assets/api/indexes_api.md b/tdesign-component/example/assets/api/indexes_api.md index eeb9a7294..771f6eb82 100644 --- a/tdesign-component/example/assets/api/indexes_api.md +++ b/tdesign-component/example/assets/api/indexes_api.md @@ -12,7 +12,7 @@ | capsuleTheme | bool? | false | 锚点是否为胶囊式样式 | | indexList | List? | - | 索引字符列表。不传默认 A-Z | | indexListMaxHeight | double? | 0.8 | 索引列表最大高度(父容器高度的百分比,默认 0.8) | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onChange | void Function(String index)? | - | 索引发生变更时触发事件 | | onSelect | void Function(String index)? | - | 点击侧边栏时触发事件 | | reverse | bool? | false | 反方向滚动置顶 | @@ -20,8 +20,6 @@ | sticky | bool? | true | 锚点是否吸顶 | | stickyOffset | double? | 0 | 锚点吸顶时与顶部的距离 | -``` -``` ### TIndexesAnchor #### 简介 @@ -33,12 +31,10 @@ | activeIndex | ValueNotifier | - | 选中索引 | | builderAnchor | Widget? Function(BuildContext context, String index, bool isPinnedToTop)? | - | 索引锚点构建 | | capsuleTheme | bool | - | 是否为胶囊式样式 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | sticky | bool | - | 索引是否吸顶 | | text | String | - | 锚点文本 | -``` -``` ### TIndexesList #### 简介 @@ -51,5 +47,5 @@ | builderIndex | Widget Function(BuildContext context, String index, bool isActive)? | - | 索引文本自定义构建,包括索引激活左侧提示 | | indexList | List | - | 索引字符列表。不传默认 A-Z | | indexListMaxHeight | double | 0.8 | 索引列表最大高度(父容器高度的百分比,默认0.8) | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onSelect | void Function(String newIndex, String oldIndex) | - | 点击侧边栏时触发事件 | diff --git a/tdesign-component/example/assets/api/input_api.md b/tdesign-component/example/assets/api/input_api.md index 7d6f2ccf8..019509b1e 100644 --- a/tdesign-component/example/assets/api/input_api.md +++ b/tdesign-component/example/assets/api/input_api.md @@ -27,7 +27,7 @@ | inputDecoration | InputDecoration? | - | 自定义输入框样式,默认圆角 | | inputFormatters | List? | - | 显示输入内容,如限制长度(LengthLimitingTextInputFormatter(6)) | | inputType | TextInputType? | - | 键盘类型,数字、字母 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | labelWidget | Widget? | - | leftLabel右侧组件,支持自定义 | | leftContentSpace | double? | - | 输入框内容左侧间距 | | leftIcon | Widget? | - | 带图标的输入框 | @@ -52,9 +52,44 @@ | selectionControls | TextSelectionControls? | - | 自定义选择控制器 | | showBottomDivider | bool | true | 是否展示底部分割线 | | size | TInputSize | TInputSize.large | 输入框规格 | -| spacer | TInputSpacer | - | 组件各模块间间距 | +| spacer | TInputSpacer? | - | 组件各模块间间距 | | textAlign | TextAlign? | - | 文字对齐方向 | | textInputBackgroundColor | Color? | - | 文本框背景色 | | textStyle | TextStyle? | - | 文本颜色 | | type | TInputType | TInputType.normal | 输入框类型 | | width | double? | - | 输入框宽度(TCardStyle时必须设置该参数) | + + +### TInputType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| normal | - | +| twoLine | - | +| longText | - | +| special | - | +| normalMaxTwoLine | - | +| cardStyle | - | + + +### TInputSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| small | - | +| large | - | + + +### TCardStyle +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| topText | - | +| topTextWithBlueBorder | - | +| errorStyle | - | diff --git a/tdesign-component/example/assets/api/link_api.md b/tdesign-component/example/assets/api/link_api.md index 7324abe45..2b2ac08fe 100644 --- a/tdesign-component/example/assets/api/link_api.md +++ b/tdesign-component/example/assets/api/link_api.md @@ -7,7 +7,7 @@ | color | Color? | - | link 文本的颜色,如果不设置则根据状态和风格进行计算 | | fontSize | double? | - | link 文本的字体大小,如果不设置则根据状态和风格进行计算 | | iconSize | double? | - | link icon 大小,如果不设置则根据状态和风格进行计算 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | label | String | - | link 展示的文本 | | leftGapWithIcon | double? | - | 前置icon和文本之间的间隔,如果不设置则根据状态和风格进行计算 | | linkClick | LinkClick? | - | link 被点击之后所采取的动作,会将uri当做参数传入到该方法当中 | @@ -19,3 +19,60 @@ | suffixIcon | Icon? | - | 后置 icon | | type | TLinkType | TLinkType.basic | link 类型 | | uri | Uri? | - | link 跳转的uri | + + +### TLinkType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| basic | - | +| withUnderline | - | +| withPrefixIcon | - | +| withSuffixIcon | - | + + +### TLinkStyle +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| primary | - | +| defaultStyle | - | +| danger | - | +| warning | - | +| success | - | + + +### TLinkState +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| normal | - | +| active | - | +| disabled | - | + + +### TLinkSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| small | - | +| medium | - | +| large | - | + + +### LinkClick +#### 简介 +限制Function类型,防止传递错误的Function,导致参数对不上 +#### 类型定义 + +```dart +typedef LinkClick = Function(Uri? uri); +``` diff --git a/tdesign-component/example/assets/api/loading_api.md b/tdesign-component/example/assets/api/loading_api.md index 6bf7be714..f5fd3bf6f 100644 --- a/tdesign-component/example/assets/api/loading_api.md +++ b/tdesign-component/example/assets/api/loading_api.md @@ -9,8 +9,34 @@ | duration | int | 2000 | 一次刷新的时间,控制动画速度 | | icon | TLoadingIcon? | TLoadingIcon.circle | 图标,支持圆形、点状、菊花状 | | iconColor | Color? | - | 图标颜色 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | refreshWidget | Widget? | - | 失败刷新组件 | | size | TLoadingSize | - | 尺寸 | | text | String? | - | 文案 | | textColor | Color? | - | 文案颜色 | + + +### TLoadingSize +#### 简介 +Loading 尺寸 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| small | 小尺寸 | +| medium | 中尺寸 | +| large | 大尺寸 | + + +### TLoadingIcon +#### 简介 +Loading图标 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| circle | 圆形 | +| point | 点状 | +| activity | 菊花状 | diff --git a/tdesign-component/example/assets/api/message_api.md b/tdesign-component/example/assets/api/message_api.md index a327735d1..c1cfed62a 100644 --- a/tdesign-component/example/assets/api/message_api.md +++ b/tdesign-component/example/assets/api/message_api.md @@ -8,7 +8,7 @@ | content | String? | - | 通知内容 | | duration | int? | 3000 | 消息内置计时器 | | icon | dynamic | true | 自定义消息前面的图标 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | link | dynamic | - | 链接名称 | | marquee | MessageMarquee? | - | 跑马灯效果 | | offset | List? | - | 相对于 placement 的偏移量 | @@ -21,12 +21,26 @@ #### 静态方法 -| 名称 | 返回类型 | 参数 | 说明 | +##### TMessage.showMessage + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| showMessage | | required BuildContext context, String? content, bool? visible, int? duration, dynamic closeBtn, dynamic icon, dynamic link, MessageMarquee? marquee, List? offset, MessageTheme? theme, VoidCallback? onCloseBtnClick, VoidCallback? onDurationEnd, VoidCallback? onLinkClick, | | +| context | BuildContext | - | - | +| content | String? | - | 通知内容 | +| visible | bool? | - | 是否显示 | +| duration | int? | - | 消息内置计时器 | +| closeBtn | dynamic | - | 关闭按钮 | +| icon | dynamic | - | 自定义消息前面的图标 | +| link | dynamic | - | 链接名称 | +| marquee | MessageMarquee? | - | 跑马灯效果 | +| offset | List? | - | 相对于 placement 的偏移量 | +| theme | MessageTheme? | - | 消息组件风格 info/success/warning/error | +| onCloseBtnClick | VoidCallback? | - | 点击关闭按钮触发 | +| onDurationEnd | VoidCallback? | - | 计时结束后触发 | +| onLinkClick | VoidCallback? | - | 点击链接文本时触发 | -``` -``` ### MessageMarquee #### 默认构造方法 @@ -37,8 +51,6 @@ | loop | int? | - | 循环次数 | | speed | int? | - | 速度 | -``` -``` ### MessageLink #### 默认构造方法 @@ -48,3 +60,17 @@ | color | Color? | - | 颜色 | | name | String | - | 名称 | | uri | Uri? | - | 资源链接 | + + +### MessageTheme +#### 简介 +定义消息主题枚举 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| info | 普通通知 | +| success | 成功通知 | +| warning | 警示通知 | +| error | 错误通知 | diff --git a/tdesign-component/example/assets/api/navbar_api.md b/tdesign-component/example/assets/api/navbar_api.md index 49fab9f65..5bc4678a1 100644 --- a/tdesign-component/example/assets/api/navbar_api.md +++ b/tdesign-component/example/assets/api/navbar_api.md @@ -12,7 +12,7 @@ | centerTitle | bool | true | 标题是否居中 | | flexibleSpace | Widget? | - | 固定背景 | | height | double | 48 | 高度 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftBarItems | List? | - | 左边操作项 | | onBack | VoidCallback? | - | 返回事件 | | opacity | double | 1.0 | 透明度 | @@ -29,8 +29,6 @@ | useBorderStyle | bool | false | 是否使用边框模式 | | useDefaultBack | bool | true | 是否使用默认的返回 | -``` -``` ### TNavBarItem #### 默认构造方法 @@ -43,4 +41,12 @@ | iconColor | Color? | - | 图标颜色 | | iconSize | double? | 24.0 | 图标尺寸 | | iconWidget | Widget? | - | 图标组件,优先级高于 icon | -| padding | EdgeInsetsGeometry? | - | | +| padding | EdgeInsetsGeometry? | - | 内部填充 | + + +### TBarItemAction +#### 类型定义 + +```dart +typedef TBarItemAction = void Function(); +``` diff --git a/tdesign-component/example/assets/api/notice-bar_api.md b/tdesign-component/example/assets/api/notice-bar_api.md index f43768900..5f72f1e01 100644 --- a/tdesign-component/example/assets/api/notice-bar_api.md +++ b/tdesign-component/example/assets/api/notice-bar_api.md @@ -1,7 +1,5 @@ ## API ### TNoticeBar -#### 简介 - #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | @@ -11,7 +9,7 @@ | direction | Axis? | Axis.horizontal | 滚动方向 | | height | double | 22 | 文字高度 (当使用prefixIcon或suffixIcon时,icon大小值等于该属性) | | interval | int? | 3000 | 步进滚动间隔时间(毫秒) | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | left | Widget? | - | 左侧内容(自定义左侧内容,优先级高于prefixIcon) | | marquee | bool? | false | 跑马灯效果 | | maxLines | int? | 1 | 文本行数(仅静态有效) | @@ -23,8 +21,6 @@ | suffixIcon | IconData? | - | 右侧图标 | | theme | TNoticeBarTheme? | TNoticeBarTheme.info | 主题 | -``` -``` ### TNoticeBarStyle #### 简介 @@ -43,6 +39,38 @@ #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TNoticeBarStyle.generateTheme | 根据主题生成样式 | +##### TNoticeBarStyle.generateTheme + +根据主题生成样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | 上下文 | +| theme | TNoticeBarTheme? | TNoticeBarTheme.info | - | + + +### TNoticeBarType +#### 简介 +公告栏类型 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| none | 静止(默认) | +| scroll | 滚动 | +| step | 步进 | + + +### TNoticeBarTheme +#### 简介 +公告栏主题 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| info | 信息(默认) | +| success | 成功 | +| warning | 警告 | +| error | 错误 | diff --git a/tdesign-component/example/assets/api/picker_api.md b/tdesign-component/example/assets/api/picker_api.md index d09bcdf8f..57fbf9802 100644 --- a/tdesign-component/example/assets/api/picker_api.md +++ b/tdesign-component/example/assets/api/picker_api.md @@ -4,25 +4,23 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| cancel | Widget? | - | 工具栏左侧自定义插槽,默认使用 [TResourceDelegate.cancel] | -| confirm | Widget? | - | 工具栏右侧自定义插槽,默认使用 [TResourceDelegate.confirm] | +| cancel | Widget? | - | 工具栏左侧自定义插槽,默认使用 [TResourceDelegate.cancel] 可用于渲染图标、图标+文字组合等。点击事件依然由外层 [GestureDetector] 处理,触发 [onCancel] 回调——所以插槽内的 Widget 不需要自己处理点击。 ```dart // 简单改文字 TPicker( cancel: const Text('关闭'), onCancel: () => Navigator.of(context).pop(), ) // 带图标 TPicker( cancel: const Icon(Icons.close, size: 22), onCancel: () => Navigator.of(context).pop(), ) ``` | +| confirm | Widget? | - | 工具栏右侧自定义插槽,默认使用 [TResourceDelegate.confirm] 可用于渲染图标、图标+文字组合等。点击事件依然由外层 [GestureDetector] 处理,触发 [onConfirm] 回调——所以插槽内的 Widget 不需要自己处理点击。 ```dart // 简单改文字 TPicker( confirm: const Text('确定'), onConfirm: (v) => Navigator.of(context).pop(v), ) // 带图标 TPicker( confirm: const Icon(Icons.check, size: 22), onConfirm: (v) => Navigator.of(context).pop(v), ) ``` | | disabled | bool | false | 是否禁用整个选择器(禁止滚动和操作),默认 false | | height | double | 200 | 视窗高度,默认 200 | | initialValue | List? | - | 初始选中值列表(按 value 匹配) | | itemBuilder | ItemBuilderType? | - | 自定义子项构建器(disabled 项仍由内部统一渲染,不会走此 builder) | | itemCount | int | 5 | 每屏显示 item 数,默认 5 | | itemDistanceCalculator | ItemDistanceCalculator? | - | 自定义距离计算器(控制颜色/字重/字号随"离中心距离"的变化) | -| items | TPickerItems | - | 数据源(必填) | -| key | | - | | -| onCancel | VoidCallback? | - | 点击「取消」按钮回调 | -| onChange | void Function(TPickerValue)? | - | 值改变回调(滚动时实时触发) | -| onConfirm | void Function(TPickerValue)? | - | 点击「确认」按钮回调 | -| onLoad | void Function(TPickerLoadEvent)? | - | 列选中项变化的事件回调 | -| title | String? | - | 工具栏中部标题(可选,不传时中部留白) | -| titleWidget | Widget? | - | 工具栏中部自定义标题插槽 | - -``` -``` +| items | TPickerItems | - | 数据源(必填) 使用密封类 [TPickerItems] 编译期强制二选一: - [TPickerColumns] → 多列独立选择 - [TPickerLinked] → 联动选择 自由结构数据通过 `.fromRaw()` 工厂构造归一化。 | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| onCancel | VoidCallback? | - | 点击「取消」按钮回调 仅作为点击事件通知,不携带任何参数。组件本身不会做任何 popup 操作,业务层可在此自行决定是否关闭弹窗、重置状态等。 | +| onChange | void Function(TPickerValue)? | - | 值改变回调(滚动时实时触发) 触发时机: - 用户滚动经过某个 enabled 项并稳定时 - disabled 修正动画完成后,回调最终落点 **注意**:此回调代表"滚动时实时变化",不代表"用户已确认选择"。 如需"已确认"语义,请使用 [onConfirm]。 如需做网络请求/埋点等去抖处理,请在业务层自行 debounce。 | +| onConfirm | void Function(TPickerValue)? | - | 点击「确认」按钮回调 携带当前选中的完整 [TPickerValue],包含: - `selectedOptions`: 当前选中的所有 [TPickerOption] - `values`: 各列选中项的 value 列表 - `labels`: 各列选中项的 label 列表 - `indexes`: 各列选中项的索引 与 [onChange] 不同——只有用户点击「确认」时才触发,代表"已确认选择"。 组件本身不会做任何 popup 操作,业务层可在此自行决定是否关闭弹窗、 提交表单等。 | +| onLoad | void Function(TPickerLoadEvent)? | - | 列选中项变化的事件回调 **触发时机**:每次用户滚动到一个 enabled 项后都会触发(联动模式下还会 在新展开的列就位后触发)。组件本身不做"距底部多少项"的阈值判断——把 决策权交给业务层。 **事件参数**包含: - [TPickerLoadEvent.column]:触发列索引 - [TPickerLoadEvent.remaining]:当前列距底部剩余项数 - [TPickerLoadEvent.displayedCount]:当前列总项数 - [TPickerLoadEvent.parentValue]:联动模式下父级选中值(首列为 null) **典型用法**:业务层根据 [TPickerLoadEvent.remaining] 自行判断是否加载更多。 ```dart onLoad: (e) async { if (e.remaining > 5 \|\| _isLoading) return; // 距底部还远 / 已在加载,跳过 _isLoading = true; final more = await fetchMore(parent: e.parentValue); setState(() { _data.addAll(more); _isLoading = false; }); } ``` | +| title | String? | - | 工具栏中部标题(可选,不传时中部留白) 顶部工具栏永远显示,包含「取消」「标题」「确认」三块。 用户点击「取消」触发 [onCancel],点击「确认」触发 [onConfirm]。 选择器与弹窗(popup)完全解耦——关闭/打开弹窗的逻辑由业务层在 这两个回调中自行控制。 典型用法(与 popup 弹窗组合): ```dart TPicker( items: items, title: '请选择地区', onCancel: () => setState(() => visible = false), onConfirm: (value) { setState(() { selected = value; visible = false; }); }, ) ``` | +| titleWidget | Widget? | - | 工具栏中部自定义标题插槽 传入后会**完全替换**默认的 [title] 文字,可用于渲染更复杂的标题(副标题、图标+文字等)。 标题区域不响应点击。 | + ### TPickerOption #### 默认构造方法 @@ -33,8 +31,6 @@ | label | String | - | 展示文字(可包含 emoji、单位、国际化等) | | value | dynamic | - | 业务值(onChange 回调返回此字段) | -``` -``` ### TPickerValue #### 默认构造方法 @@ -44,8 +40,13 @@ | indexes | List | - | 每列选中项的索引 | | selectedOptions | List | - | 每列选中的完整 option | -``` -``` +#### 公开属性 + +| 属性 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| labels | List | - | 所有选中项的 label(展示用) 顺序与列顺序对应,可直接用于 UI 展示。 懒计算并缓存,生命周期内只计算一次。 | +| values | List | - | 所有选中项的 value(提交表单用) 顺序与列顺序对应,可直接用于表单提交。 懒计算并缓存,生命周期内只计算一次。 | + ### TPickerLoadEvent #### 默认构造方法 @@ -54,63 +55,71 @@ | --- | --- | --- | --- | | column | int | - | 触发事件的列索引(0 表示第一列) | | displayedCount | int | - | 当前列已展示的选项总数 | -| parentValue | dynamic | - | 当前列的父级选中值(联动模式下使用) | +| parentValue | dynamic | - | 当前列的父级选中值(联动模式下使用) 第一列时为 null;业务层可用此值从原始数据中筛选子级选项。 | | remaining | int | - | 距底部剩余的选项数(业务可用此值做"接近底部时加载"判断) | -``` -``` ### TPickerColumns #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| columns | List> | columns | 每列的选项列表 | +| columns | List> | - | 每列的选项列表 | #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TPickerColumns.fromRaw | 从自由结构的 raw 数据创建,自动归一化 +##### TPickerColumns.fromRaw + +从自由结构的 raw 数据创建,自动归一化 ```dart TPickerColumns.fromRaw( [['北京', '上海', '广州']], keys: const TPickerKeys(label: 'name', value: 'code'), ) - ``` | + ``` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| rawColumns | List | - | - | +| keys | TPickerKeys | TPickerKeys.defaults | - | -``` -``` ### TPickerLinked #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| tree | Map | tree | 联动树结构:`Map` | +| tree | Map | - | 联动树结构:`Map` value 可以是: - `Map` → 下一级联动 - `List` → 叶子级选项 | #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TPickerLinked.fromRaw | 从自由结构的 raw Map 数据创建,自动归一化 +##### TPickerLinked.fromRaw + +从自由结构的 raw Map 数据创建,自动归一化 ```dart TPickerLinked.fromRaw({ '广东': {'深圳': ['南山', '福田'], '广州': ['天河']}, '浙江': {'杭州': ['西湖']}, }) - ``` | + ``` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| rawTree | Map | - | - | +| keys | TPickerKeys | TPickerKeys.defaults | - | -``` -``` ### TPickerItems -``` -``` +#### 简介 +选择器数据源密封类 + + 编译期强制二选一,消除运行时类型错误: + - [TPickerColumns] → 多列独立选择 + - [TPickerLinked] → 联动选择 ### TPickerKeys #### 默认构造方法 @@ -121,3 +130,9 @@ | disabled | String | 'disabled' | 禁用标记对应的字段名,默认 `disabled` | | label | String | 'label' | 展示文案对应的字段名,默认 `label` | | value | String | 'value' | 业务值对应的字段名,默认 `value` | + +#### 静态成员 + +| 名称 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| defaults | TPickerKeys | - | 默认配置(`label / value / disabled / children`) | diff --git a/tdesign-component/example/assets/api/popover_api.md b/tdesign-component/example/assets/api/popover_api.md index 0a44f139d..bcf1ff4a9 100644 --- a/tdesign-component/example/assets/api/popover_api.md +++ b/tdesign-component/example/assets/api/popover_api.md @@ -1,20 +1,33 @@ ## API ### TPopover -#### 简介 - #### 静态方法 -| 名称 | 返回类型 | 参数 | 说明 | +##### TPopover.showPopover + +返回类型:`Future` + +| 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| showPopover | | required BuildContext context, String? content, Widget? contentWidget, double offset, TPopoverTheme? theme, bool closeOnClickOutside, TPopoverPlacement? placement, bool? showArrow, double arrowSize, EdgeInsetsGeometry? padding, double? width, double? height, Color? overlayColor, OnTap? onTap, OnLongTap? onLongTap, BorderRadius? radius, | | +| context | BuildContext | - | 上下文 | +| content | String? | - | 显示内容 | +| contentWidget | Widget? | - | 自定义内容 | +| offset | double | 4 | 偏移 | +| theme | TPopoverTheme? | - | 弹出气泡主题 | +| closeOnClickOutside | bool | true | - | +| placement | TPopoverPlacement? | - | 浮层出现位置 | +| showArrow | bool? | true | 是否显示浮层箭头 | +| arrowSize | double | 8 | 箭头大小 | +| padding | EdgeInsetsGeometry? | - | 内容内边距 | +| width | double? | - | 内容宽度(包含padding,实际高度:height - paddingLeft - paddingRight) | +| height | double? | - | 内容高度(包含padding,实际高度:height - paddingTop - paddingBottom) | +| overlayColor | Color? | Colors.transparent | - | +| onTap | OnTap? | - | 点击事件 | +| onLongTap | OnLongTap? | - | 长按事件 | +| radius | BorderRadius? | - | 圆角 | -``` -``` ### TPopoverWidget -#### 简介 - #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | @@ -24,7 +37,7 @@ | contentWidget | Widget? | - | 自定义内容 | | context | BuildContext | - | 上下文 | | height | double? | - | 内容高度(包含padding,实际高度:height - paddingTop - paddingBottom) | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | offset | double | 4 | 偏移 | | onLongTap | OnLongTap? | - | 长按事件 | | onTap | OnTap? | - | 点击事件 | @@ -34,3 +47,53 @@ | showArrow | bool? | true | 是否显示浮层箭头 | | theme | TPopoverTheme? | - | 弹出气泡主题 | | width | double? | - | 内容宽度(包含padding,实际高度:height - paddingLeft - paddingRight) | + + +### TPopoverTheme +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| dark | 暗色 | +| light | 亮色 | +| info | 品牌色 | +| success | 成功 | +| warning | 警告 | +| error | 错误 | + + +### TPopoverPlacement +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| topLeft | 上左 | +| top | 上 | +| topRight | 上右 | +| rightTop | 右上 | +| right | 右 | +| rightBottom | 右下 | +| bottomRight | 下右 | +| bottom | 下 | +| bottomLeft | 下左 | +| leftBottom | 左下 | +| left | 左 | +| leftTop | 左上 | + + +### OnTap +#### 类型定义 + +```dart +typedef OnTap = Function(String? content); +``` + + +### OnLongTap +#### 类型定义 + +```dart +typedef OnLongTap = Function(String? content); +``` diff --git a/tdesign-component/example/assets/api/popup_api.md b/tdesign-component/example/assets/api/popup_api.md index e36a6e212..9210f148c 100644 --- a/tdesign-component/example/assets/api/popup_api.md +++ b/tdesign-component/example/assets/api/popup_api.md @@ -1,98 +1,148 @@ ## API -### TSlidePopupRoute +### TPopup #### 简介 -从屏幕的某个方向滑动弹出的Dialog框的路由,比如从顶部、底部、左、右滑出页面 -#### 默认构造方法 +弹出层入口:五向滑入 / 居中弹出,支持蒙层、bottom 操作区、center 面板外下方关闭区。 -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| barrierClick | VoidCallback? | - | 蒙层点击事件,仅在[modalBarrierFull]为false时触发 | -| barrierLabel | | - | | -| builder | WidgetBuilder | - | 控件构建器 | -| close | VoidCallback? | - | 关闭前事件 | -| focusMove | bool | false | 是否有输入框获取焦点时整体平移避免输入框被遮挡 | -| isDismissible | bool | true | 点击蒙层能否关闭 | -| modalBarrierColor | Color? | Colors.black54 | 蒙层颜色 | -| modalBarrierFull | bool | false | 是否全屏显示蒙层 | -| modalHeight | double? | - | 弹出框高度 | -| modalLeft | double? | 0 | 弹出框左侧距离 | -| modalTop | double? | 0 | 弹出框顶部距离 | -| modalWidth | double? | - | 弹出框宽度 | -| open | VoidCallback? | - | 打开前事件 | -| opened | VoidCallback? | - | 打开后事件 | -| slideTransitionFrom | SlideTransitionFrom | SlideTransitionFrom.bottom | 设置从屏幕的哪个方向滑出 | + 通过 [show] 命令式打开;返回 [TPopupHandle] 用于关闭与再次打开。 + 多次调用 [show] 会继续压入新的浮层路由,可用于叠加展示。 -``` -``` + **示例** -### TPopupBottomDisplayPanel -#### 简介 -右上角带关闭的底部浮层面板 -#### 默认构造方法 + ```dart + final handle = TPopup.show( + context, + options: TPopupOptions.bottom( + titleWidget: const Text('标题'), + child: MyPanel(), + ), + ); + handle.close(); + handle.open(); + ``` -| 参数 | 类型 | 默认值 | 说明 | + 配置项见 [TPopupOptions];方向见 [TPopupPlacement]。 + +#### 工厂构造方法 + +| 名称 | 说明 | +| --- | --- | +| TPopup._ | | + + +#### 静态方法 + +| 名称 | 返回类型 | 参数 | 说明 | | --- | --- | --- | --- | -| backgroundColor | | - | | -| child | | - | | -| closeClick | PopupClick? | - | 关闭按钮点击回调 | -| closeColor | Color? | - | 关闭按钮颜色 | -| closeSize | double? | - | 关闭按钮图标尺寸 | -| draggable | | - | | -| fixedHeight | | - | | -| hideClose | bool | false | 是否隐藏关闭按钮 | -| key | | - | | -| maxHeightRatio | | - | | -| minHeightRatio | | - | | -| radius | | - | | -| title | | - | | -| titleColor | | - | | -| titleFontSize | double? | - | 标题字体大小 | -| titleLeft | bool | false | 标题是否靠左 | +| show | | required BuildContext context, required TPopupOptions options, BuildContext? navigatorContext, bool useRootNavigator, | 打开浮层并压入独立 [PopupRoute]。 [context] 用于查找 [Navigator] 并展示浮层。 [options] 浮层配置;方向固定时推荐 [TPopupOptions.bottom] 等命名工厂。 返回 [TPopupHandle],可用 [TPopupHandle.close]、[TPopupHandle.open]、 [TPopupHandle.isShowing] 控制与查询。 重复调用会继续 push 新的浮层;若需互斥请在业务层管理。 [navigatorContext] 可选,指定承载浮层的 [Navigator] 的 context,默认 [context]。 [useRootNavigator] 为 true 时使用根 [Navigator](嵌套导航场景)。 | ``` ``` -### TPopupBottomConfirmPanel +### TPopupOptions #### 简介 -带确认的底部浮层面板 +[TPopup.show] 的配置对象。 + + ## 如何创建 + + | 场景 | 推荐用法 | + |------|----------| + | 弹出方向已知 | [TPopupOptions.bottom]、[TPopupOptions.center]、[TPopupOptions.top]、[TPopupOptions.left]、[TPopupOptions.right] | + | 方向由变量决定 | 默认构造并设置 [placement];传错字段会在 [TPopup.show] / [TPopupHandle.open] 时抛 [FlutterError] | + + 命名工厂只暴露当前方向生效的字段(例如 [TPopupOptions.bottom] 无 [width] 参数)。 + + ## 字段与 [TPopupPlacement] + + | [TPopupPlacement] | 头部 / 关闭区 | 尺寸 | + |-------------------|-------------|------| + | [TPopupPlacement.bottom] | [headerBuilder]、[titleWidget]、[cancelBuilder]、[confirmBuilder] | [height]、[inset] | + | [TPopupPlacement.center] | [closeBuilder] | [width]、[height] | + | [TPopupPlacement.top] | — | [height]、[inset] | + | [TPopupPlacement.left]、[TPopupPlacement.right] | — | [width]、[inset] | + + ## Builder 三态([headerBuilder]、[cancelBuilder]、[confirmBuilder]、[closeBuilder]) + + | 传参方式 | 效果 | + |----------|------| + | 省略(使用默认值) | 渲染内置 UI | + | 显式 `null` | 隐藏该区域 | + | 自定义 [TPopupHeaderBuilder] / [TPopupSlotBuilder] | 完全替换;需自行提供交互与语义,可调用 `close` 关闭浮层 | + + [titleWidget] 默认为 `null`,表示无标题内容。 + + 生命周期回调见 [onOpen]、[onOpened]、[onClose]、[onClosed]、[onVisibleChange]、[onOverlayClick]。 #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| backgroundColor | | - | | -| child | | - | | -| draggable | | - | | -| key | | - | | -| leftClick | PopupClick? | - | 左边文本点击回调 | -| leftText | String? | - | 左边文本 | -| leftTextColor | Color? | - | 左边文本颜色 | -| leftTextFontSize | double? | - | 左边文本字体大小 | -| maxHeightRatio | | - | | -| minHeightRatio | | - | | -| radius | | - | | -| rightClick | PopupClick? | - | 右边文本点击回调 | -| rightText | String? | - | 右边文本 | -| rightTextColor | Color? | - | 右边文本颜色 | -| rightTextFontSize | double? | - | 右边文本字体大小 | -| title | | - | | -| titleColor | | - | | -| titleFontSize | double? | - | 标题字体大小 | +| animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 | +| backgroundColor | Color? | - | 内容区背景色,默认主题容器色。 | +| cancelBuilder | TPopupSlotBuilder? | _kPopupDefaultCancel | bottom 左侧操作槽;仅 [headerBuilder] 为内置默认时生效。 | +| child | Widget | - | 浮层主体内容(必填)。 | +| closeBuilder | TPopupSlotBuilder? | _kPopupDefaultClose | center 面板外下方关闭区;仅 [TPopupPlacement.center] 生效。三态见类文档「Builder 三态」。 | +| closeOnOverlayClick | | - | | +| confirmBuilder | TPopupSlotBuilder? | _kPopupDefaultConfirm | bottom 右侧操作槽;仅 [headerBuilder] 为内置默认时生效。 | +| destroyOnClose | bool | false | 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 | +| headerBuilder | TPopupHeaderBuilder? | _kPopupDefaultHeader | bottom 头部;仅 [TPopupPlacement.bottom] 生效。三态见类文档「Builder 三态」。 | +| height | double? | - | 高度;[TPopupPlacement.top]、[TPopupPlacement.bottom] 生效;[TPopupPlacement.center] 约束面板尺寸。 | +| inset | TPopupInset? | - | 交叉轴边缘留白;具体类型由 [placement] 决定。 | +| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 | +| onClose | VoidCallback? | - | 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 | +| onClosed | VoidCallback? | - | 当前展示周期真正结束。 | +| onOpen | VoidCallback? | - | 路由 push 时(打开动画开始前)。 | +| onOpened | VoidCallback? | - | 打开动画结束。 | +| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 | +| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 [TPopupTrigger]。 | +| overlayColor | Color? | - | 蒙层颜色,默认 black54。 | +| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 | +| placement | TPopupPlacement | TPopupPlacement.bottom | 出现位置,默认 [TPopupPlacement.bottom]。 | +| radius | double? | - | 内容区圆角,默认主题大圆角。 | +| showOverlay | bool | true | 是否绘制半透明蒙层。 | +| titleWidget | Widget? | - | bottom 标题插槽;仅 [headerBuilder] 为内置默认时生效。`null` 表示无标题。 | +| width | double? | - | 宽度;[TPopupPlacement.left]、[TPopupPlacement.right]、[TPopupPlacement.center] 生效。 | + + +#### 工厂构造方法 + +| 名称 | 说明 | +| --- | --- | +| TPopupOptions.bottom | 创建 [TPopupPlacement.bottom] 配置。 + + 固定 [placement] 为 [TPopupPlacement.bottom];默认带内置头部。 + 蒙层、动画、生命周期等字段语义见同名成员文档。 | +| TPopupOptions.center | 创建 [TPopupPlacement.center] 配置。 + + 固定 [placement] 为 [TPopupPlacement.center];默认展示面板外下方圆形关闭按钮。 | +| TPopupOptions.left | 创建 [TPopupPlacement.left] 配置。 + + 固定 [placement] 为 [TPopupPlacement.left];未传 [width] 时布局默认宽度 280。 | +| TPopupOptions.right | 创建 [TPopupPlacement.right] 配置。 + + 固定 [placement] 为 [TPopupPlacement.right];未传 [width] 时布局默认宽度 280。 | +| TPopupOptions.top | 创建 [TPopupPlacement.top] 配置。 + + 固定 [placement] 为 [TPopupPlacement.top];无内置头部。 | ``` ``` -### TPopupCenterPanel +### TPopupHandle #### 简介 -居中浮层面板 -#### 默认构造方法 +[TPopup.show] 的返回值,用于控制同一份 [TPopupOptions] 的多次打开与关闭。 -| 参数 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| backgroundColor | Color? | - | 背景颜色 | -| child | Widget | - | 子控件 | -| closeClick | PopupClick? | - | 关闭按钮点击回调 | -| closeColor | Color? | - | 关闭按钮颜色 | -| closeSize | double? | - | 关闭按钮图标尺寸 | -| closeUnderBottom | bool | false | 关闭按钮是否在视图框下方 | -| key | | - | | -| radius | double? | - | 圆角 | + **示例** + + ```dart + final handle = TPopup.show( + context, + options: TPopupOptions.bottom(child: panel), + ); + handle.close(); + handle.open(); // 可省略 context,复用已缓存的 Navigator + ``` + +#### 工厂构造方法 + +| 名称 | 说明 | +| --- | --- | +| TPopupHandle._ | | diff --git a/tdesign-component/example/assets/api/progress_api.md b/tdesign-component/example/assets/api/progress_api.md index 44b2617be..090884e04 100644 --- a/tdesign-component/example/assets/api/progress_api.md +++ b/tdesign-component/example/assets/api/progress_api.md @@ -4,12 +4,12 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| animationDuration | int? | 300 | 动画持续时间(正整数,单位为毫秒) | +| animationDuration | int | 300 | 动画持续时间(正整数,单位为毫秒) | | backgroundColor | Color? | - | 进度条背景颜色 | | circleRadius | double? | - | 环形进度条半径(正数) | | color | Color? | - | 进度条颜色 | | customProgressLabel | Widget? | - | 自定义标签 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | label | TLabelWidget? | - | 进度条标签 | | labelWidgetAlignment | Alignment? | - | 自定义标签对齐方式 | | labelWidgetWidth | double? | - | 自定义标签宽度 | @@ -22,3 +22,38 @@ | strokeWidth | double? | - | 进度条粗细(正数) | | type | TProgressType | - | 进度条类型 | | value | double? | - | 进度值(0.0 到 1.0 之间的正数) | + + +### TProgressType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| linear | - | +| circular | - | +| micro | - | +| button | - | + + +### TProgressLabelPosition +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| inside | - | +| left | - | +| right | - | + + +### TProgressStatus +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| primary | - | +| warning | - | +| danger | - | +| success | - | diff --git a/tdesign-component/example/assets/api/pull-down-refresh_api.md b/tdesign-component/example/assets/api/pull-down-refresh_api.md index 3ade18761..c819e7919 100644 --- a/tdesign-component/example/assets/api/pull-down-refresh_api.md +++ b/tdesign-component/example/assets/api/pull-down-refresh_api.md @@ -8,38 +8,38 @@ TDesign刷新头部 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | backgroundColor | Color? | - | 背景颜色 | -| clamping | | - | | +| clamping | bool? | - | - | | completeDuration | Duration? | - | 完成延时 | | enableHapticFeedback | bool | true | 开启震动反馈 | | enableInfiniteRefresh | bool | false | 是否开启无限刷新 | | extent | double? | 48.0 | Header容器高度 | | float | bool | false | 是否悬浮 | -| frictionFactor | | - | | -| hapticFeedback | | - | | -| hitOver | | - | | -| horizontalFrictionFactor | | - | | -| horizontalReadySpringBuilder | | - | | -| horizontalSpring | | - | | -| infiniteHitOver | | - | | +| frictionFactor | - | - | - | +| hapticFeedback | bool? | - | - | +| hitOver | - | - | - | +| horizontalFrictionFactor | - | - | - | +| horizontalReadySpringBuilder | - | - | - | +| horizontalSpring | - | - | - | +| infiniteHitOver | bool? | - | - | | infiniteOffset | double? | - | 无限刷新偏移量 | | key | Key? | - | Key | -| listenable | | - | | +| listenable | - | - | - | | loadingIcon | TLoadingIcon | TLoadingIcon.circle | loading样式 | -| maxOverOffset | | - | | -| notifyWhenInvisible | | - | | +| maxOverOffset | - | - | - | +| notifyWhenInvisible | - | - | - | | overScroll | bool | true | 越界滚动([enableInfiniteRefresh]为true或[infiniteOffset]有值时生效) | -| position | | - | | -| processedDuration | | - | | -| readySpringBuilder | | - | | -| safeArea | | false | | -| secondaryCloseTriggerOffset | | - | | -| secondaryDimension | | - | | -| secondaryTriggerOffset | | - | | -| secondaryVelocity | | - | | -| spring | | - | | -| springRebound | | - | | +| position | - | - | - | +| processedDuration | Duration? | - | - | +| readySpringBuilder | - | - | - | +| safeArea | - | false | - | +| secondaryCloseTriggerOffset | - | - | - | +| secondaryDimension | - | - | - | +| secondaryTriggerOffset | - | - | - | +| secondaryVelocity | - | - | - | +| spring | - | - | - | +| springRebound | - | - | - | | triggerDistance | double | 48.0 | 触发刷新任务的偏移量,同[triggerOffset] | -| triggerOffset | | - | | -| triggerWhenReach | | - | | -| triggerWhenRelease | | - | | -| triggerWhenReleaseNoWait | | - | | +| triggerOffset | double? | - | - | +| triggerWhenReach | - | - | - | +| triggerWhenRelease | - | - | - | +| triggerWhenReleaseNoWait | - | - | - | diff --git a/tdesign-component/example/assets/api/radio_api.md b/tdesign-component/example/assets/api/radio_api.md index 74529e483..3ae5d569b 100644 --- a/tdesign-component/example/assets/api/radio_api.md +++ b/tdesign-component/example/assets/api/radio_api.md @@ -6,34 +6,32 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| backgroundColor | | - | | -| cardMode | | - | | -| checkBoxLeftSpace | | - | | -| contentDirection | | TContentDirection.right | | -| customContentBuilder | | - | | -| customIconBuilder | | - | | -| customSpace | | - | | -| disableColor | | - | | -| enable | | true | | -| id | | - | | -| insetSpacing | | - | | -| key | | - | | +| backgroundColor | Color? | - | - | +| cardMode | bool? | - | - | +| checkBoxLeftSpace | double? | - | - | +| contentDirection | TContentDirection | TContentDirection.right | - | +| customContentBuilder | ContentBuilder? | - | - | +| customIconBuilder | IconBuilder? | - | - | +| customSpace | EdgeInsetsGeometry? | - | - | +| disableColor | Color? | - | - | +| enable | bool | true | - | +| id | String? | - | - | +| insetSpacing | double? | - | - | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | radioStyle | TRadioStyle | TRadioStyle.circle | 单选框按钮样式 | -| selectColor | | - | | -| showDivider | bool | - | 是否显示下划线 | -| size | | TCheckBoxSize.small | | -| spacing | | - | | -| subTitle | | - | | -| subTitleColor | | - | | -| subTitleFont | | - | | -| subTitleMaxLine | | 1 | | -| title | | - | | -| titleColor | | - | | -| titleFont | | - | | -| titleMaxLine | | 1 | | +| selectColor | Color? | - | - | +| showDivider | bool? | - | - | +| size | TCheckBoxSize | TCheckBoxSize.small | - | +| spacing | double? | - | - | +| subTitle | String? | - | - | +| subTitleColor | Color? | - | - | +| subTitleFont | Font? | - | - | +| subTitleMaxLine | int | 1 | - | +| title | String? | - | - | +| titleColor | Color? | - | - | +| titleFont | Font? | - | - | +| titleMaxLine | int | 1 | - | -``` -``` ### TRadioGroup #### 简介 @@ -46,22 +44,42 @@ RadioGroup分组对象,继承自TCheckboxGroup,字段含义与父类一致 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| cardMode | | false | | -| child | | - | | -| contentDirection | | - | | -| controller | | - | | -| customContentBuilder | | - | | -| customIconBuilder | | - | | -| direction | | - | | -| directionalTdRadios | | - | | +| cardMode | bool | false | - | +| child | Widget? | - | - | +| contentDirection | TContentDirection? | - | - | +| controller | TCheckboxGroupController? | - | - | +| customContentBuilder | ContentBuilder? | - | - | +| customIconBuilder | IconBuilder? | - | - | +| direction | Axis? | - | - | +| directionalTdRadios | List? | - | - | | divider | Widget? | - | 自定义下划线 | -| key | | - | | -| onRadioGroupChange | | - | | -| passThrough | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| onRadioGroupChange | OnRadioGroupChange? | - | - | +| passThrough | bool? | - | - | | radioCheckStyle | TRadioStyle? | - | 勾选样式 | | rowCount | int | 1 | 每行几列 | -| selectId | | - | | +| selectId | String? | - | - | | showDivider | bool | false | 是否显示下划线 | -| spacing | | - | | +| spacing | double? | - | - | | strictMode | bool | true | 严格模式下,用户不能取消勾选,只能切换选择项, | -| titleMaxLine | | - | | +| titleMaxLine | int? | - | - | + + +### TRadioStyle +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| circle | - | +| square | - | +| check | - | +| hollowCircle | - | + + +### OnRadioGroupChange +#### 类型定义 + +```dart +typedef OnRadioGroupChange = void Function(String? selectedId); +``` diff --git a/tdesign-component/example/assets/api/rate_api.md b/tdesign-component/example/assets/api/rate_api.md index d3089c22e..6fd415996 100644 --- a/tdesign-component/example/assets/api/rate_api.md +++ b/tdesign-component/example/assets/api/rate_api.md @@ -5,7 +5,7 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | allowHalf | bool? | false | 是否允许半选 | -| builderText | Widget Function(BuildContext context, double value)? | - | 评分等级对应的辅助文字自定义构建,优先级高于[texts] | +| builderText | Widget Function(BuildContext context, double value)? | - | 评分等级对应的辅助文字自定义构建,优先级高于[texts] 配置后,会忽略[texts],[textWidth],[iconTextGap] | | color | List? | - | 评分图标的颜色,示例:[选中颜色] / [选中颜色,未选中颜色],默认:[TTheme.of(context).warningColor5, TTheme.of(context).grayColor4] | | count | int? | 5 | 评分的数量 | | crossAxisAlignment | CrossAxisAlignment? | CrossAxisAlignment.center | 评分图标与辅助文字的交叉轴对齐方式 | @@ -14,13 +14,24 @@ | gap | double? | - | 评分图标的间距,默认:TTheme.of(context).spacer8 | | icon | List? | - | 自定义评分图标,[选中和未选中图标] / [选中图标,未选中图标],默认:[TIcons.star_filled] | | iconTextGap | double? | - | 评分图标与辅助文字的间距,默认:[TTheme.of(context).spacer16] | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | mainAxisAlignment | MainAxisAlignment? | MainAxisAlignment.start | 评分图标与辅助文字的主轴对齐方式 | | mainAxisSize | MainAxisSize? | MainAxisSize.min | 评分图标与辅助文字主轴方向上如何占用空间 | | onChange | void Function(double value)? | - | 评分数改变时触发 | | placement | PlacementEnum? | PlacementEnum.top | 选择评分弹框的位置,值为[PlacementEnum.none]表示不显示评分弹框。 | | showText | bool? | false | 是否显示对应的辅助文字 | | size | double? | 24.0 | 评分图标的大小 | -| texts | List? | const ['极差', '失望', '一般', '满意', '惊喜'] | 评分等级对应的辅助文字, | +| texts | List? | const ['极差', '失望', '一般', '满意', '惊喜'] | 评分等级对应的辅助文字, 当[allowHalf]为false时长度应与[count]一致, 当[allowHalf]为true时长度应为[count]的两倍, 自定义值示例:['1分', '2分', '3分', '4分', '5分']。 | | textWidth | double? | 48.0 | 评分等级对应的辅助文字宽度 | | value | double? | 0 | 选择评分的值 | + + +### PlacementEnum +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| none | - | +| top | - | +| bottom | - | diff --git a/tdesign-component/example/assets/api/result_api.md b/tdesign-component/example/assets/api/result_api.md index 836cd2d1f..6b8595043 100644 --- a/tdesign-component/example/assets/api/result_api.md +++ b/tdesign-component/example/assets/api/result_api.md @@ -6,7 +6,19 @@ | --- | --- | --- | --- | | description | String? | - | 描述文本,用于提供额外信息 | | icon | Widget? | - | 图标组件,用于在结果中显示一个图标 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | theme | TResultTheme | TResultTheme.defaultTheme | 主题样式,默认主题样式为defaultTheme | | title | String | '' | 标题文本,显示结果的主要信息,默认标题为空字符串 | | titleStyle | TextStyle? | - | 自定义字体样式,用于设置标题文本的样式 | + + +### TResultTheme +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| defaultTheme | - | +| success | - | +| warning | - | +| error | - | diff --git a/tdesign-component/example/assets/api/search_api.md b/tdesign-component/example/assets/api/search_api.md index 08e6d630e..dd658ea95 100644 --- a/tdesign-component/example/assets/api/search_api.md +++ b/tdesign-component/example/assets/api/search_api.md @@ -14,7 +14,7 @@ | enabled | bool? | - | 是否禁用 | | focusNode | FocusNode? | - | 自定义焦点 | | inputAction | TextInputAction? | - | 键盘动作类型 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | mediumStyle | bool | false | 是否在导航栏中的样式 | | needCancel | bool | false | 是否需要取消按钮 | | onActionClick | TSearchBarEvent? | - | 自定义操作回调 | @@ -28,3 +28,51 @@ | placeHolder | String? | - | 预设文案 | | readOnly | bool? | - | 是否只读 | | style | TSearchStyle? | TSearchStyle.square | 样式 | + + +### TSearchStyle +#### 简介 +搜索框的样式 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| square | 方形 | +| round | 圆形 | + + +### TSearchAlignment +#### 简介 +搜索框对齐方式 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| left | 默认头部对齐 | +| center | 居中 | + + +### TSearchBarEvent +#### 类型定义 + +```dart +typedef TSearchBarEvent = void Function(String value); +``` + + +### TSearchBarClearEvent +#### 类型定义 + +```dart +typedef TSearchBarClearEvent = bool? Function(String value); +``` + + +### TSearchBarCallBack +#### 类型定义 + +```dart +typedef TSearchBarCallBack = void Function(); +``` diff --git a/tdesign-component/example/assets/api/side-bar_api.md b/tdesign-component/example/assets/api/side-bar_api.md index d36520c3d..76f810131 100644 --- a/tdesign-component/example/assets/api/side-bar_api.md +++ b/tdesign-component/example/assets/api/side-bar_api.md @@ -9,7 +9,7 @@ | controller | TSideBarController? | - | 控制器 | | defaultValue | int? | - | 默认值 | | height | double? | - | 高度 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | loading | bool? | - | 加载效果 | | loadingWidget | Widget? | - | 自定义加载动画 | | onChanged | ValueChanged? | - | 选中值发生变化(Controller控制) | @@ -22,8 +22,6 @@ | unSelectedColor | Color? | - | 未选中颜色 | | value | int? | - | 选项值 | -``` -``` ### TSideBarItem #### 默认构造方法 @@ -33,7 +31,17 @@ | badge | TBadge? | - | 徽标 | | disabled | bool | false | 是否禁用 | | icon | IconData? | - | 图标 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | label | String | '' | 标签 | | textStyle | TextStyle? | - | 标签样式 | | value | int | -1 | 值 | + + +### TSideBarStyle +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| normal | - | +| outline | - | diff --git a/tdesign-component/example/assets/api/skeleton_api.md b/tdesign-component/example/assets/api/skeleton_api.md index 7801ab935..5b13db47b 100644 --- a/tdesign-component/example/assets/api/skeleton_api.md +++ b/tdesign-component/example/assets/api/skeleton_api.md @@ -6,18 +6,29 @@ | --- | --- | --- | --- | | animation | TSkeletonAnimation? | - | 动画效果 | | delay | int | 0 | 延迟显示加载时间 | -| key | | - | | -| theme | | TSkeletonTheme.text | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| theme | TSkeletonTheme | TSkeletonTheme.text | - | + +#### 公开属性 + +| 属性 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| rowCol | TSkeletonRowCol | - | 自定义行列数量、宽度高度、间距等 | #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TSkeleton.fromRowCol | 从行列框架创建骨架屏 | +##### TSkeleton.fromRowCol + +从行列框架创建骨架屏 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| animation | TSkeletonAnimation? | - | 动画效果 | +| delay | int | 0 | 延迟显示加载时间 | +| rowCol | TSkeletonRowCol | - | 自定义行列数量、宽度高度、间距等 | -``` -``` ### TSkeletonRowColStyle #### 默认构造方法 @@ -26,8 +37,6 @@ | --- | --- | --- | --- | | rowSpacing | double Function(BuildContext) | _defaultRowSpacing | 行间距 | -``` -``` ### TSkeletonRowCol #### 默认构造方法 @@ -37,8 +46,6 @@ | objects | List> | - | 行列对象 | | style | TSkeletonRowColStyle | const TSkeletonRowColStyle() | 样式 | -``` -``` ### TSkeletonRowColObjStyle #### 默认构造方法 @@ -51,15 +58,36 @@ #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TSkeletonRowColObjStyle.circle | 圆形 | -| TSkeletonRowColObjStyle.rect | 矩形 | -| TSkeletonRowColObjStyle.spacer | 空白占位符 | -| TSkeletonRowColObjStyle.text | 文本 | +##### TSkeletonRowColObjStyle.circle + +圆形 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| background | Color Function(BuildContext) | _defaultBackground | 背景颜色 | + + +##### TSkeletonRowColObjStyle.rect + +矩形 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| background | Color Function(BuildContext) | _defaultBackground | 背景颜色 | + + +##### TSkeletonRowColObjStyle.spacer + +空白占位符 + +##### TSkeletonRowColObjStyle.text + +文本 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| background | Color Function(BuildContext) | _defaultBackground | 背景颜色 | -``` -``` ### TSkeletonRowColObj #### 默认构造方法 @@ -75,9 +103,78 @@ #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TSkeletonRowColObj.circle | 圆形 | -| TSkeletonRowColObj.rect | 矩形 | -| TSkeletonRowColObj.spacer | 空白占位符 | -| TSkeletonRowColObj.text | 文本 | +##### TSkeletonRowColObj.circle + +圆形 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| width | double? | 48 | 宽度 | +| height | double? | 48 | 高度 | +| flex | int? | - | 弹性因子 | +| margin | EdgeInsets | EdgeInsets.zero | 间距 | +| style | TSkeletonRowColObjStyle | const TSkeletonRowColObjStyle.circle() | 样式 | + + +##### TSkeletonRowColObj.rect + +矩形 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| width | double? | - | 宽度 | +| height | double? | 16 | 高度 | +| flex | int? | 1 | 弹性因子 | +| margin | EdgeInsets | EdgeInsets.zero | 间距 | +| style | TSkeletonRowColObjStyle | const TSkeletonRowColObjStyle.rect() | 样式 | + + +##### TSkeletonRowColObj.spacer + +空白占位符 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| width | double? | - | 宽度 | +| height | double? | - | 高度 | +| flex | int? | - | 弹性因子 | +| margin | EdgeInsets | EdgeInsets.zero | 间距 | + + +##### TSkeletonRowColObj.text + +文本 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| width | double? | - | 宽度 | +| height | double? | 16 | 高度 | +| flex | int? | 1 | 弹性因子 | +| margin | EdgeInsets | EdgeInsets.zero | 间距 | +| style | TSkeletonRowColObjStyle | const TSkeletonRowColObjStyle.text() | 样式 | + + +### TSkeletonAnimation +#### 简介 +骨架图动画 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| gradient | 渐变 | +| flashed | 闪烁 | + + +### TSkeletonTheme +#### 简介 +骨架图风格 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| avatar | 头像 | +| image | 图片 | +| text | 文本 | +| paragraph | 段落 | diff --git a/tdesign-component/example/assets/api/slider_api.md b/tdesign-component/example/assets/api/slider_api.md index 7b00a6314..e7ee4332e 100644 --- a/tdesign-component/example/assets/api/slider_api.md +++ b/tdesign-component/example/assets/api/slider_api.md @@ -5,19 +5,17 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | boxDecoration | Decoration? | - | 自定义盒子样式 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftLabel | String? | - | 左侧标签 | -| onChanged | ValueChanged? | - | 滑动变化监听 | -| onChangeEnd | ValueChanged? | - | 滑动结束监听 | -| onChangeStart | ValueChanged? | - | 滑动开始监听 | -| onTap | Function(Position position, Offset offset, double value)? | - | Thumb 点击事件 位置、坐标、当前值 | -| onThumbTextTap | Function(Position position, Offset offset, double value)? | - | Thumb 点击浮标文字 位置、坐标、当前值 | +| onChanged | ValueChanged? | - | 滑动变化监听 | +| onChangeEnd | ValueChanged? | - | 滑动结束监听 | +| onChangeStart | ValueChanged? | - | 滑动开始监听 | +| onTap | Function(Offset offset, double value)? | - | Thumb 点击事件 坐标、当前值 | +| onThumbTextTap | Function(Offset offset, double value)? | - | Thumb 点击浮标文字 坐标、当前值 | | rightLabel | String? | - | 右侧标签 | | sliderThemeData | TSliderThemeData? | - | 样式 | -| value | RangeValues | - | 默认值 | +| value | double | - | 默认值 | -``` -``` ### TRangeSlider #### 默认构造方法 @@ -25,13 +23,23 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | boxDecoration | Decoration? | - | 自定义盒子样式 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftLabel | String? | - | 左侧标签 | | onChanged | ValueChanged? | - | 滑动变化监听 | | onChangeEnd | ValueChanged? | - | 滑动结束监听 | | onChangeStart | ValueChanged? | - | 滑动开始监听 | -| onTap | Function(Position position, Offset offset, double value)? | - | Thumb 点击事件 位置、坐标、当前值 | -| onThumbTextTap | Function(Position position, Offset offset, double value)? | - | Thumb 点击浮标文字 位置、坐标、当前值 | +| onTap | Function(Position position, Offset offset, double value)? | - | Thumb 点击事件 位置、坐标、当前值 | +| onThumbTextTap | Function(Position position, Offset offset, double value)? | - | Thumb 点击浮标文字 位置、坐标、当前值 | | rightLabel | String? | - | 右侧标签 | | sliderThemeData | TSliderThemeData? | - | 样式 | | value | RangeValues | - | 默认值 | + + +### Position +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| start | - | +| end | - | diff --git a/tdesign-component/example/assets/api/stepper_api.md b/tdesign-component/example/assets/api/stepper_api.md index 0ebfa602c..95debede2 100644 --- a/tdesign-component/example/assets/api/stepper_api.md +++ b/tdesign-component/example/assets/api/stepper_api.md @@ -10,7 +10,7 @@ | disableInput | bool | false | 禁用输入框 | | eventController | StreamController? | - | 事件控制器 | | inputWidth | double? | - | 禁用全部操作 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | max | int | 100 | 最大值 | | min | int | 0 | 最小值 | | onBlur | VoidCallback? | - | 输入框失去焦点时触发 | @@ -20,3 +20,70 @@ | step | int | 1 | 步长 | | theme | TStepperTheme | TStepperTheme.normal | 组件风格 | | value | int? | 0 | 值 | + + +### TStepperSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| small | - | +| medium | - | +| large | - | + + +### TStepperTheme +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| normal | - | +| filled | - | +| outline | - | + + +### TStepperIconType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| remove | - | +| add | - | + + +### TStepperOverlimitType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| minus | - | +| plus | - | + + +### TStepperEventType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| cleanValue | - | + + +### TStepperOverlimitFunction +#### 类型定义 + +```dart +typedef TStepperOverlimitFunction = void Function(TStepperOverlimitType type); +``` + + +### TTapFunction +#### 类型定义 + +```dart +typedef TTapFunction = void Function(); +``` diff --git a/tdesign-component/example/assets/api/steps_api.md b/tdesign-component/example/assets/api/steps_api.md index 62115981f..6cd49f650 100644 --- a/tdesign-component/example/assets/api/steps_api.md +++ b/tdesign-component/example/assets/api/steps_api.md @@ -6,15 +6,13 @@ | --- | --- | --- | --- | | activeIndex | int | 0 | 步骤条当前激活的索引 | | direction | TStepsDirection | TStepsDirection.horizontal | 步骤条方向 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | readOnly | bool | false | 步骤条readOnly模式 | | simple | bool | false | 步骤条simple模式 | | status | TStepsStatus | TStepsStatus.success | 步骤条状态 | | steps | List | - | 步骤条数据 | | verticalSelect | bool | false | 步骤条垂直自定义步骤条选择模式 | -``` -``` ### TStepsItemData #### 默认构造方法 @@ -27,3 +25,27 @@ | errorIcon | IconData? | - | 失败图标 | | successIcon | IconData? | - | 成功图标 | | title | String? | - | 标题 | + + +### TStepsDirection +#### 简介 +Steps步骤条方向 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| horizontal | - | +| vertical | - | + + +### TStepsStatus +#### 简介 +steps步骤条状态 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| success | - | +| error | - | diff --git a/tdesign-component/example/assets/api/swipe-cell_api.md b/tdesign-component/example/assets/api/swipe-cell_api.md index b23f2a9e4..5f6f0c9ca 100644 --- a/tdesign-component/example/assets/api/swipe-cell_api.md +++ b/tdesign-component/example/assets/api/swipe-cell_api.md @@ -8,16 +8,16 @@ | --- | --- | --- | --- | | cell | Widget | - | 单元格 [TCell] | | closeWhenOpened | bool? | true | 当同一组([groupTag])中的一个[TSwipeCell]打开时,是否关闭组中的所有其他[TSwipeCell] | -| closeWhenTapped | bool? | true | 当同一组([groupTag])中的一个[TSwipeCell]被点击时,是否应该关闭组中的所有[TSwipeCell] | +| closeWhenTapped | bool? | true | 当同一组([groupTag])中的一个[TSwipeCell]被点击时,是否应该关闭组中的所有[TSwipeCell] [cell]组件被点击时必须传递点击事件,执行`TSwipeCellInherited.of(context)?.cellClick()` | | controller | SlidableController? | - | 自定义控制滑动窗口 | | direction | Axis? | Axis.horizontal | 可拖动的方向 | | disabled | bool? | false | 是否禁用滑动 | | dragStartBehavior | DragStartBehavior? | DragStartBehavior.start | 处理拖动开始行为的方式[GestureDetector.dragStartBehavior] | | duration | Duration? | const Duration(milliseconds: 200) | 打开关闭动画时长 | | groupTag | Object? | - | 组,配置后,[closeWhenOpened]、[closeWhenTapped]才起作用 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | left | TSwipeCellPanel? | - | 左侧滑动操作项面板 | -| onChange | Function(TSwipeDirection direction, bool open)? | - | 滑动展开事件 | +| onChange | Function(TSwipeDirection direction, bool open)? | - | 滑动展开事件 | | opened | List? | const [false, false] | 默认打开,[left, right] | | right | TSwipeCellPanel? | - | 右侧滑动操作项面板 | | slidableKey | Key? | - | 滑动组件的 Key | @@ -25,7 +25,36 @@ #### 静态方法 -| 名称 | 返回类型 | 参数 | 说明 | +##### TSwipeCell.close + +根据[groupTag]关闭[TSwipeCell] + + current:保留当前不关闭 + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| tag | Object? | - | - | +| current | SlidableController? | - | - | + + +##### TSwipeCell.of + +获取上下文最近的[controller] + +返回类型:`SlidableController?` + +| 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| close | | required Object? tag, SlidableController? current, | 根据[groupTag]关闭[TSwipeCell] current:保留当前不关闭 | -| of | | required BuildContext context, | 获取上下文最近的[controller] | +| context | BuildContext | - | - | + + +### TSwipeDirection +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| right | - | +| left | - | diff --git a/tdesign-component/example/assets/api/swiper_api.md b/tdesign-component/example/assets/api/swiper_api.md index 5a23bdff5..a6b8650eb 100644 --- a/tdesign-component/example/assets/api/swiper_api.md +++ b/tdesign-component/example/assets/api/swiper_api.md @@ -6,13 +6,20 @@ TDesign风格的Swiper指示器样式,与flutter_swiper的Swiper结合使用 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| alignment | Alignment? | - | 当 scrollDirection== Axis.horizontal 时,默认Alignment.bottomCenter | +| alignment | Alignment? | - | 当 scrollDirection== Axis.horizontal 时,默认Alignment.bottomCenter 当 scrollDirection== Axis.vertical 时,默认Alignment.centerRight | | builder | SwiperPlugin | TSwiperPagination.dots | 具体样式 | -| key | Key? | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | margin | EdgeInsetsGeometry | const EdgeInsets.all(10.0) | 指示器和container之间的距离 | -``` -``` +#### 静态成员 + +| 名称 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| controls | SwiperPlugin | - | 箭头样式 | +| dots | SwiperPlugin | - | 圆点样式 | +| dotsBar | SwiperPlugin | - | 圆角矩形 + 圆点样式 默认宽度20,高度6 | +| fraction | SwiperPlugin | - | 数字样式 | + ### TPageTransformer #### 简介 @@ -28,7 +35,20 @@ TD默认PageTransformer #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TPageTransformer.margin | 普通margin的卡片式 | -| TPageTransformer.scaleAndFade | 缩放或透明的卡片式 | +##### TPageTransformer.margin + +普通margin的卡片式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| margin | double? | 6.0 | 左右间隔 | + + +##### TPageTransformer.scaleAndFade + +缩放或透明的卡片式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| fade | double? | 1 | 淡化比例 | +| scale | double? | 0.8 | 缩放比例 | diff --git a/tdesign-component/example/assets/api/switch_api.md b/tdesign-component/example/assets/api/switch_api.md index fb8cb07d3..9491b21f0 100644 --- a/tdesign-component/example/assets/api/switch_api.md +++ b/tdesign-component/example/assets/api/switch_api.md @@ -7,7 +7,7 @@ | closeText | String? | - | 关闭文案 | | enable | bool | true | 是否可点击 | | isOn | bool | false | 是否打开 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onChanged | OnSwitchChanged? | - | 改变事件 | | openText | String? | - | 打开文案 | | size | TSwitchSize? | TSwitchSize.medium | 尺寸:大、中、小 | @@ -18,3 +18,36 @@ | trackOffColor | Color? | - | 关闭时轨道颜色 | | trackOnColor | Color? | - | 开启时轨道颜色 | | type | TSwitchType? | TSwitchType.fill | 类型:填充、文本、加载 | + + +### TSwitchSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | - | +| medium | - | +| small | - | + + +### TSwitchType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| fill | - | +| text | - | +| loading | - | +| icon | - | + + +### OnSwitchChanged +#### 简介 +开关改变事件处理 +#### 类型定义 + +```dart +typedef OnSwitchChanged = bool Function(bool value); +``` diff --git a/tdesign-component/example/assets/api/tab-bar_api.md b/tdesign-component/example/assets/api/tab-bar_api.md index dad57643b..84af968b6 100644 --- a/tdesign-component/example/assets/api/tab-bar_api.md +++ b/tdesign-component/example/assets/api/tab-bar_api.md @@ -4,11 +4,11 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | +| basicType | TBottomTabBarBasicType | - | 基本样式(纯文本、纯图标、图标+文本) | | animationCurve | Curve | Curves.easeInOutCubic | 动画曲线 | | animationDuration | Duration | const Duration(milliseconds: 300) | 动画时长 | | backgroundColor | Color? | - | 背景颜色 (可选) | | barHeight | double? | _kDefaultTabBarHeight | tab高度 | -| basicType | TBottomTabBarBasicType | basicType | 基本样式(纯文本、纯图标、图标+文本) | | centerDistance | double? | - | icon与文本中间距离(可选) | | componentType | TBottomTabBarComponentType? | TBottomTabBarComponentType.label | 选项样式 默认label | | currentIndex | int? | - | 选中的index(可选) | @@ -16,7 +16,7 @@ | dividerHeight | double? | - | 分割线高度(可选) | | dividerThickness | double? | - | 分割线厚度(可选) | | indicatorAnimation | TBottomTabBarIndicatorAnimation | TBottomTabBarIndicatorAnimation.none | 指示器动画类型 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | navigationTabs | List | - | tabs配置 | | needInkWell | bool | false | 是否需要水波纹效果 | | outlineType | TBottomTabBarOutlineType? | TBottomTabBarOutlineType.filled | 标签栏样式 默认filled | @@ -28,8 +28,6 @@ | useSafeArea | bool | true | 使用安全区域 | | useVerticalDivider | bool? | - | 是否使用竖线分隔(如果选项样式为 label,则强制为 false) | -``` -``` ### BadgeConfig #### 默认构造方法 @@ -41,8 +39,6 @@ | showBadge | bool | - | 是否展示消息 | | tBadge | TBadge? | - | 消息样式(未设置但 showBadge 为 true,则默认使用红点) | -``` -``` ### TBottomTabBarTabConfig #### 默认构造方法 @@ -60,8 +56,6 @@ | unselectedIcon | Widget? | - | 未选中时图标 | | unselectTabTextStyle | TextStyle? | - | 文本未选择样式 basicType为text时必填 | -``` -``` ### TBottomTabBarPopUpBtnConfig #### 默认构造方法 @@ -72,8 +66,6 @@ | onChanged | ValueChanged | - | 统一在 onChanged 中处理各item点击事件 | | popUpDialogConfig | TBottomTabBarPopUpShapeConfig? | - | 弹窗UI配置 | -``` -``` ### TBottomTabBarPopUpShapeConfig #### 默认构造方法 @@ -87,8 +79,6 @@ | popUpWidth | double? | - | 弹窗宽度(不设置,默认为按钮宽度 - 20) | | radius | double? | - | panel圆角 默认0 | -``` -``` ### PopUpMenuItem #### 默认构造方法 @@ -97,5 +87,48 @@ | --- | --- | --- | --- | | alignment | AlignmentGeometry | AlignmentDirectional.center | 对齐方式 | | itemWidget | Widget? | - | 选项widget | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | value | String | - | 选项值 | + + +### TBottomTabBarBasicType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| text | 单层级纯文本标签栏 | +| iconText | 文本加图标标签栏 | +| icon | 纯图标标签栏 | +| expansionPanel | 双层级纯文本标签栏 | + + +### TBottomTabBarComponentType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| normal | 普通样式 | +| label | 带胶囊背景的item选中样式 | + + +### TBottomTabBarOutlineType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| filled | 填充样式 | +| capsule | 胶囊样式 | + + +### TBottomTabBarIndicatorAnimation +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| none | 无动画,瞬间切换 | +| linear | 线性滑动:指示器匀速从一个 tab 滑到另一个 | +| elastic | 弹性动画:指示器先拉伸后收缩 | diff --git a/tdesign-component/example/assets/api/table_api.md b/tdesign-component/example/assets/api/table_api.md index 30239b18d..b5b15b344 100644 --- a/tdesign-component/example/assets/api/table_api.md +++ b/tdesign-component/example/assets/api/table_api.md @@ -12,7 +12,7 @@ | empty | TTableEmpty? | - | 空表格呈现样式 | | footerWidget | Widget? | - | 自定义表尾 | | height | double? | - | 表格高度,超出后会出现滚动条 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | loading | bool? | false | 加载中状态 | | loadingWidget | Widget? | - | 自定义加载中状态 | | onCellTap | OnCellTap? | - | 单元格点击事件 | @@ -24,8 +24,6 @@ | stripe | bool? | false | 斑马纹 | | width | double? | - | 表格宽度 | -``` -``` ### TTableCol #### 默认构造方法 @@ -45,8 +43,6 @@ | title | String? | - | 表头标题 | | width | double? | - | 列宽 | -``` -``` ### TTableEmpty #### 默认构造方法 @@ -55,3 +51,73 @@ | --- | --- | --- | --- | | assetUrl | String? | - | 空状态图片 | | text | String? | - | 空状态文字 | + + +### TTableColFixed +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| left | - | +| right | - | +| none | - | + + +### TTableColAlign +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| left | - | +| center | - | +| right | - | + + +### SelectableFunc +#### 类型定义 + +```dart +typedef SelectableFunc = bool Function(int index, dynamic row); +``` + + +### RowCheckFunc +#### 类型定义 + +```dart +typedef RowCheckFunc = bool Function(int index, dynamic row); +``` + + +### OnCellTap +#### 类型定义 + +```dart +typedef OnCellTap = void Function(int rowIndex, dynamic row, TTableCol col); +``` + + +### OnScroll +#### 类型定义 + +```dart +typedef OnScroll = void Function(ScrollController controller); +``` + + +### OnSelect +#### 类型定义 + +```dart +typedef OnSelect = void Function(List? data); +``` + + +### OnRowSelect +#### 类型定义 + +```dart +typedef OnRowSelect = void Function(int index, bool checked); +``` diff --git a/tdesign-component/example/assets/api/tabs_api.md b/tdesign-component/example/assets/api/tabs_api.md index b55e32e45..2bddc3c38 100644 --- a/tdesign-component/example/assets/api/tabs_api.md +++ b/tdesign-component/example/assets/api/tabs_api.md @@ -16,24 +16,22 @@ | indicatorPadding | EdgeInsets? | - | 引导padding | | indicatorWidth | double? | - | tabBar下标宽度 | | isScrollable | bool | false | 是否滚动 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | labelColor | Color? | - | tabBar 已选标签颜色 | | labelPadding | EdgeInsetsGeometry? | - | tab间距 | | labelStyle | TextStyle? | - | 已选label字体 | -| onTap | Function(int)? | - | 点击事件 | +| onTap | Function(int)? | - | 点击事件 | | outlineType | TTabBarOutlineType | TTabBarOutlineType.filled | 选项卡样式 | | physics | ScrollPhysics? | - | 自定义滑动 | | selectedBgColor | Color? | - | 被选中背景色,只有outlineType为capsule时有效 | | showIndicator | bool | false | 是否展示引导控件 | -| tabAlignment | | - | | +| tabAlignment | TabAlignment? | - | - | | tabs | List | - | tab数组 | | unSelectedBgColor | Color? | - | 未选中背景色,只有outlineType为capsule时有效 | | unselectedLabelColor | Color? | - | tabBar未选标签颜色 | | unselectedLabelStyle | TextStyle? | - | unselectedLabel字体 | | width | double? | - | tabBar宽度 | -``` -``` ### TTab #### 默认构造方法 @@ -47,14 +45,12 @@ | height | double? | - | tab高度 | | icon | Widget? | - | 图标 | | iconMargin | EdgeInsetsGeometry | const EdgeInsets.only(bottom: 4.0, right: 4.0) | 图标间距 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | outlineType | TTabOutlineType | TTabOutlineType.filled | 选项卡样式 | | size | TTabSize | TTabSize.small | 选项卡尺寸 | | text | String? | - | 文字内容 | | textMargin | EdgeInsetsGeometry? | - | 中间内容宽度 | -``` -``` ### TTabBarView #### 默认构造方法 @@ -64,4 +60,36 @@ | children | List | - | 子widget列表 | | controller | TabController? | - | 控制器 | | isSlideSwitch | bool | false | 是否可以滑动切换 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | + + +### TTabSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | - | +| small | - | + + +### TTabOutlineType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| filled | 填充样式 | +| capsule | 胶囊样式 | +| card | 卡片 | + + +### TTabBarOutlineType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| filled | 填充样式 | +| capsule | 胶囊样式 | +| card | 卡片 | diff --git a/tdesign-component/example/assets/api/tag_api.md b/tdesign-component/example/assets/api/tag_api.md index 0493284b7..35b04aeab 100644 --- a/tdesign-component/example/assets/api/tag_api.md +++ b/tdesign-component/example/assets/api/tag_api.md @@ -4,6 +4,7 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | +| text | String | - | 标签内容 | | backgroundColor | Color? | - | 背景颜色,优先级高于style的backgroundColor | | disable | bool | false | 是否为禁用状态 | | fixedWidth | double? | - | 标签的固定宽度 | @@ -14,7 +15,7 @@ | iconWidget | Widget? | - | 自定义图标内容,需自处理颜色 | | isLight | bool | false | 是否为浅色 | | isOutline | bool | false | 是否为描边类型,默认不是 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | needCloseIcon | bool | false | 关闭图标 | | onCloseTap | GestureTapCallback? | - | 关闭图标点击事件 | | overflow | TextOverflow? | - | 文字溢出处理 | @@ -22,18 +23,16 @@ | shape | TTagShape | TTagShape.square | 标签形状 | | size | TTagSize | TTagSize.medium | 标签大小 | | style | TTagStyle? | - | 标签样式 | -| text | String | text | 标签内容 | | textColor | Color? | - | 文字颜色,优先级高于style的textColor | | theme | TTagTheme? | - | 主题 | -``` -``` ### TSelectTag #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | +| text | String | - | 标签内容 | | disableSelect | bool | false | 是否禁用选择 | | disableSelectStyle | TTagStyle? | - | 不可选标签样式 | | fixedWidth | double? | - | 标签的固定宽度 | @@ -43,7 +42,7 @@ | isLight | bool | false | 是否为浅色 | | isOutline | bool | false | 是否为描边类型,默认不是 | | isSelected | bool | false | 是否选中 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | needCloseIcon | bool | false | 关闭图标 | | onCloseTap | GestureTapCallback? | - | 关闭图标点击事件 | | onSelectChanged | ValueChanged? | - | 标签点击,选中状态改变时的回调 | @@ -51,12 +50,9 @@ | selectStyle | TTagStyle? | - | 选中的标签样式 | | shape | TTagShape | TTagShape.square | 标签形状 | | size | TTagSize | TTagSize.medium | 标签大小 | -| text | String | text | 标签内容 | | theme | TTagTheme? | - | 主题 | | unSelectStyle | TTagStyle? | - | 未选中标签样式 | -``` -``` ### TTagStyle #### 默认构造方法 @@ -72,11 +68,89 @@ | fontWeight | FontWeight? | - | 字体粗细 | | textColor | Color? | - | 文字颜色 | +#### 公开属性 + +| 属性 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| closeIconColor | Color? | - | 关闭图标颜色 | + #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TTagStyle.generateDisableSelectStyle | 根据主题生成禁用Tag样式 | -| TTagStyle.generateFillStyleByTheme | 根据主题生成填充Tag样式 | -| TTagStyle.generateOutlineStyleByTheme | 根据主题生成描边Tag样式 | +##### TTagStyle.generateDisableSelectStyle + +根据主题生成禁用Tag样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | 上下文,方便获取主题内容 | +| isLight | bool | - | - | +| isOutline | bool | - | - | +| shape | TTagShape | - | - | + + +##### TTagStyle.generateFillStyleByTheme + +根据主题生成填充Tag样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | 上下文,方便获取主题内容 | +| theme | TTagTheme? | - | - | +| light | bool | - | - | +| shape | TTagShape | - | - | + + +##### TTagStyle.generateOutlineStyleByTheme + +根据主题生成描边Tag样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | 上下文,方便获取主题内容 | +| theme | TTagTheme? | - | - | +| light | bool | - | - | +| shape | TTagShape | - | - | + + +### TTagTheme +#### 简介 +Tag展示类型 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| defaultTheme | 默认 | +| primary | 常规 | +| warning | 警告 | +| danger | 危险 | +| success | 成功 | + + +### TTagSize +#### 简介 +标签尺寸 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| extraLarge | - | +| large | - | +| medium | - | +| small | - | +| custom | - | + + +### TTagShape +#### 简介 +标签形状 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| square | - | +| round | - | +| mark | - | diff --git a/tdesign-component/example/assets/api/text_api.md b/tdesign-component/example/assets/api/text_api.md index e394cf480..07aaf746c 100644 --- a/tdesign-component/example/assets/api/text_api.md +++ b/tdesign-component/example/assets/api/text_api.md @@ -4,8 +4,8 @@ | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | +| data | - | - | 以下系统 text 属性,释义请参考系统 [Text] 中注释 | | backgroundColor | Color? | - | 背景颜色 | -| data | null | data | 以下系统 text 属性,释义请参考系统 [Text] 中注释 | | font | Font? | - | 字体尺寸,包含 大小size 和 行高height | | fontFamily | FontFamily? | - | 字体ttf | | fontFamilyUrl | String? | - | 是否禁用懒加载 FontFamily 的能力 | @@ -13,64 +13,94 @@ | forceVerticalCenter | bool | false | 是否强制居中 | | isInFontLoader | bool | false | 是否在 FontLoader 中使用 | | isTextThrough | bool? | false | 是否是横线穿过样式(删除线) | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | lineThroughColor | Color? | - | 删除线颜色,对应 TestStyle 的 decorationColor | -| locale | | - | | -| maxLines | | - | | -| overflow | | - | | +| locale | Locale? | - | - | +| maxLines | int? | - | - | +| overflow | TextOverflow? | - | - | | package | String? | - | 字体包名 | -| semanticsLabel | | - | | -| softWrap | | - | | -| strutStyle | | - | | +| semanticsLabel | String? | - | - | +| softWrap | bool? | - | - | +| strutStyle | StrutStyle? | - | - | | style | TextStyle? | - | 自定义的 TextStyle,其中指定的属性,将覆盖扩展的外层属性 | -| textAlign | | - | | +| textAlign | TextAlign? | - | - | | textColor | Color? | - | 文本颜色 | -| textDirection | | - | | -| textHeightBehavior | | - | | -| textScaleFactor | | - | | -| textWidthBasis | | - | | +| textDirection | TextDirection? | - | - | +| textHeightBehavior | ui.TextHeightBehavior? | - | - | +| textScaleFactor | double? | - | - | +| textWidthBasis | TextWidthBasis? | - | - | + +#### 公开属性 + +| 属性 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| textSpan | InlineSpan? | - | - | #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TText.rich | 富文本构造方法 | +##### TText.rich + +富文本构造方法 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| textSpan | InlineSpan? | - | - | +| font | Font? | - | 字体尺寸,包含 大小size 和 行高height | +| fontWeight | FontWeight? | - | 字体粗细 | +| fontFamily | FontFamily? | - | 字体ttf | +| textColor | Color? | - | 文本颜色 | +| backgroundColor | Color? | - | 背景颜色 | +| isTextThrough | bool? | false | 是否是横线穿过样式(删除线) | +| lineThroughColor | Color? | - | 删除线颜色,对应 TestStyle 的 decorationColor | +| package | String? | - | 字体包名 | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| style | TextStyle? | - | 自定义的 TextStyle,其中指定的属性,将覆盖扩展的外层属性 | +| strutStyle | StrutStyle? | - | - | +| textAlign | TextAlign? | - | - | +| textDirection | TextDirection? | - | - | +| locale | Locale? | - | - | +| softWrap | bool? | - | - | +| overflow | TextOverflow? | - | - | +| textScaleFactor | double? | - | - | +| maxLines | int? | - | - | +| semanticsLabel | String? | - | - | +| textWidthBasis | TextWidthBasis? | - | - | +| textHeightBehavior | ui.TextHeightBehavior? | - | - | +| forceVerticalCenter | bool | false | 是否强制居中 | +| isInFontLoader | bool | false | 是否在 FontLoader 中使用 | +| fontFamilyUrl | String? | - | 是否禁用懒加载 FontFamily 的能力 | -``` -``` ### TTextSpan #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| children | | - | | -| context | | - | | -| font | | - | | -| fontFamily | | - | | -| fontWeight | | - | | -| isTextThrough | | false | | -| lineThroughColor | | - | | -| mouseCursor | | - | | -| onEnter | | - | | -| onExit | | - | | -| package | | - | | -| recognizer | | - | | -| semanticsLabel | | - | | -| style | | - | | -| text | | - | | -| textColor | | - | | +| children | List? | - | - | +| context | BuildContext? | - | - | +| font | Font? | - | - | +| fontFamily | FontFamily? | - | - | +| fontWeight | FontWeight? | - | - | +| isTextThrough | bool? | false | - | +| lineThroughColor | Color? | - | - | +| mouseCursor | MouseCursor? | - | - | +| onEnter | PointerEnterEventListener? | - | - | +| onExit | PointerExitEventListener? | - | - | +| package | String? | - | - | +| recognizer | GestureRecognizer? | - | - | +| semanticsLabel | String? | - | - | +| style | TextStyle? | - | - | +| text | String? | - | - | +| textColor | Color? | - | - | -``` -``` ### TTextConfiguration #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| child | | - | | +| child | Widget | - | - | | globalFontFamily | FontFamily? | - | 全局字体,kTextNeedGlobalFontFamily=true 时生效 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | paddingConfig | TTextPaddingConfig? | - | forceVerticalCenter=true 时,内置 padding 配置 | diff --git a/tdesign-component/example/assets/api/textarea_api.md b/tdesign-component/example/assets/api/textarea_api.md index f131dad57..0414512df 100644 --- a/tdesign-component/example/assets/api/textarea_api.md +++ b/tdesign-component/example/assets/api/textarea_api.md @@ -23,7 +23,7 @@ | inputDecoration | InputDecoration? | - | 自定义输入框TextField组件样式 | | inputFormatters | List? | - | 显示输入内容,如限制长度(LengthLimitingTextInputFormatter(6)) | | inputType | TextInputType? | - | 键盘类型,数字、字母 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | label | String? | - | 输入框标题 | | labelIcon | Widget? | - | 输入框标题图标 | | labelStyle | TextStyle? | - | 左侧标签文本样式 | @@ -48,3 +48,13 @@ | textInputBackgroundColor | Color? | - | 文本框背景色 | | textStyle | TextStyle? | - | 文本颜色 | | width | double? | - | 输入框宽度 | + + +### TTextareaLayout +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| vertical | - | +| horizontal | - | diff --git a/tdesign-component/example/assets/api/theme_api.md b/tdesign-component/example/assets/api/theme_api.md index 3dd48fc58..922b3d8be 100644 --- a/tdesign-component/example/assets/api/theme_api.md +++ b/tdesign-component/example/assets/api/theme_api.md @@ -6,22 +6,66 @@ | --- | --- | --- | --- | | child | Widget | - | 子控件 | | data | TThemeData | - | 主题数据 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | systemData | ThemeData? | - | Flutter系统主题数据 | #### 静态方法 -| 名称 | 返回类型 | 参数 | 说明 | +##### TTheme.defaultData + +获取默认主题数据,全局唯一 + +返回类型:`TThemeData` + +##### TTheme.needMultiTheme + +开启多套主题功能 + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| defaultData | | | 获取默认主题数据,全局唯一 | -| needMultiTheme | | bool value, | 开启多套主题功能 | -| of | | BuildContext? context, | 获取主题数据,如果未传context则获取全局唯一的默认数据, 传了context,则获取最近的主题,取不到则会获取全局唯一默认数据 | -| ofNullable | | BuildContext? context, | 获取主题数据,取不到则可空 传了context,则获取最近的主题,取不到或未传context则返回null, | -| setResourceBuilder | | required TResourceBuilder delegate, bool needAlwaysBuild, | 设置资源代理, needAlwaysBuild=true:每次都会走build方法;如果全局有多个Delegate,需要区分情况去获取,则可以设置needAlwaysBuild为true,业务自己判断返回哪个delegate needAlwaysBuild=false:返回delegate为null,则每次都会走build方法,返回了 | +| value | bool | true | - | + + +##### TTheme.of + +获取主题数据,如果未传context则获取全局唯一的默认数据, + 传了context,则获取最近的主题,取不到则会获取全局唯一默认数据 + +返回类型:`TThemeData` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext? | - | - | + + +##### TTheme.ofNullable + +获取主题数据,取不到则可空 + 传了context,则获取最近的主题,取不到或未传context则返回null, + +返回类型:`TThemeData?` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext? | - | - | + + +##### TTheme.setResourceBuilder + +设置资源代理, + needAlwaysBuild=true:每次都会走build方法;如果全局有多个Delegate,需要区分情况去获取,则可以设置needAlwaysBuild为true,业务自己判断返回哪个delegate + needAlwaysBuild=false:返回delegate为null,则每次都会走build方法,返回了 + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| delegate | TResourceBuilder | - | - | +| needAlwaysBuild | bool | false | - | -``` -``` ### TThemeData #### 默认构造方法 @@ -38,11 +82,64 @@ | shadowMap | TMap> | - | 阴影 | | spacerMap | TMap | - | 间隔 | +#### 公开属性 + +| 属性 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| dark | TThemeData? | - | 暗色主题 | +| light | TThemeData | - | 亮色主题 | + #### 静态方法 -| 名称 | 返回类型 | 参数 | 说明 | +##### TThemeData.defaultData + +获取默认Data,一个App里只有一个,用于没有context的地方 + +返回类型:`TThemeData` + +| 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| defaultData | | TExtraThemeData? extraThemeData, | 获取默认Data,一个App里只有一个,用于没有context的地方 | -| fromJson | | required String name, required String themeJson, String? darkName, null recoverDefault, TExtraThemeData? extraThemeData, | 解析配置的json文件为主题数据 [name] 主题名称,目前只支持一级键 [themeJson] 主题json字符串,要求json配置必须正确 [recoverDefault] 是否恢复为默认主题数据 [extraThemeData] 额外扩展的主题数据 | -| parseThemeData | | required String name, required null themeConfig, required TExtraThemeData? extraThemeData, | | +| extraThemeData | TExtraThemeData? | - | 额外定义的结构 | + + +##### TThemeData.fromJson + +解析配置的json文件为主题数据 + + [name] 主题名称,目前只支持一级键 + + [themeJson] 主题json字符串,要求json配置必须正确 + + [recoverDefault] 是否恢复为默认主题数据 + + [extraThemeData] 额外扩展的主题数据 + +返回类型:`TThemeData?` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| name | String | - | 名称 | +| themeJson | String | - | - | +| darkName | String? | - | - | +| recoverDefault | - | false | - | +| extraThemeData | TExtraThemeData? | - | 额外定义的结构 | + + +##### TThemeData.parseThemeData + +返回类型:`TThemeData` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| name | String | - | 名称 | +| themeConfig | - | - | - | +| extraThemeData | TExtraThemeData? | - | 额外定义的结构 | + + +### DefaultMapFactory +#### 类型定义 + +```dart +typedef DefaultMapFactory = TMap? Function(); +``` diff --git a/tdesign-component/example/assets/api/time-counter_api.md b/tdesign-component/example/assets/api/time-counter_api.md index 574d84ddb..5c81242ea 100644 --- a/tdesign-component/example/assets/api/time-counter_api.md +++ b/tdesign-component/example/assets/api/time-counter_api.md @@ -11,9 +11,9 @@ | controller | TTimeCounterController? | - | 控制器,可控制开始/暂停/继续/重置 | | direction | TTimeCounterDirection | TTimeCounterDirection.down | 计时方向,默认倒计时 | | format | String | 'HH:mm:ss' | 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒(分隔符必须为长度为1的非空格的字符) | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | millisecond | bool | false | 是否开启毫秒级渲染 | -| onChange | Function(int time)? | - | 时间变化时触发回调 | +| onChange | Function(int time)? | - | 时间变化时触发回调 | | onFinish | VoidCallback? | - | 计时结束时触发回调 | | size | TTimeCounterSize | TTimeCounterSize.medium | 尺寸 | | splitWithUnit | bool | false | 使用时间单位分割 | @@ -21,14 +21,10 @@ | theme | TTimeCounterTheme | TTimeCounterTheme.defaultTheme | 风格 | | time | int | - | 必需;计时时长,单位毫秒 | -``` -``` ### TTimeCounterController #### 简介 倒计时组件控制器,可控制开始(`start()`)/暂停(`pause()`)/继续(`resume()`)/重置(`reset([int? time])`) -``` -``` ### TTimeCounterStyle #### 简介 @@ -56,6 +52,66 @@ #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TTimeCounterStyle.generateStyle | 生成默认样式 | +##### TTimeCounterStyle.generateStyle + +生成默认样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | - | +| size | TTimeCounterSize? | - | - | +| theme | TTimeCounterTheme? | - | - | +| splitWithUnit | bool? | - | - | + + +### TTimeCounterStatus +#### 简介 +计时组件控制器转态 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| start | 开始 | +| pause | 暂停 | +| resume | 继续 | +| reset | 重置 | +| idle | 空,默认值 | + + +### TTimeCounterDirection +#### 简介 +计时组件计时方向 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| down | 倒计时 | +| up | 正向计时 | + + +### TTimeCounterSize +#### 简介 +计时组件尺寸 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| small | 小 | +| medium | 中等 | +| large | 大 | + + +### TTimeCounterTheme +#### 简介 +计时组件风格 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| defaultTheme | 默认 | +| round | 圆形 | +| square | 方形 | diff --git a/tdesign-component/example/assets/api/toast_api.md b/tdesign-component/example/assets/api/toast_api.md index fd2a9575d..f0699ef24 100644 --- a/tdesign-component/example/assets/api/toast_api.md +++ b/tdesign-component/example/assets/api/toast_api.md @@ -3,15 +3,176 @@ #### 静态方法 -| 名称 | 返回类型 | 参数 | 说明 | -| --- | --- | --- | --- | -| dismissAll | | | 关闭所有Toast | -| dismissLoading | | | 关闭加载Toast(向后兼容) | -| dismissToast | | required String toastId, | 关闭指定的Toast | -| showFail | | required String? text, IconTextDirection direction, required BuildContext context, Duration duration, bool? preventTap, Color? backgroundColor, int? maxLines, TextStyle? textStyle, double? iconSize, Color? iconColor, String? toastId, | 失败提示Toast | -| showIconText | | required String? text, IconData? icon, IconTextDirection direction, required BuildContext context, Duration duration, bool? preventTap, Color? backgroundColor, int? maxLines, TextStyle? textStyle, double? iconSize, Color? iconColor, String? toastId, | 带图标的Toast | -| showLoading | | required BuildContext context, String? text, Duration duration, bool? preventTap, Widget? customWidget, Color? backgroundColor, TextStyle? textStyle, double? iconSize, Color? iconColor, String? toastId, | 带文案的加载Toast | -| showLoadingWithoutText | | required BuildContext context, Duration duration, bool? preventTap, Color? backgroundColor, double? iconSize, Color? iconColor, String? toastId, | 不带文案的加载Toast | -| showSuccess | | required String? text, IconTextDirection direction, required BuildContext context, Duration duration, bool? preventTap, Color? backgroundColor, int? maxLines, TextStyle? textStyle, double? iconSize, Color? iconColor, String? toastId, | 成功提示Toast | -| showText | | required String? text, required BuildContext context, Duration duration, int? maxLines, BoxConstraints? constraints, bool? preventTap, Widget? customWidget, Color? backgroundColor, TextStyle? textStyle, String? toastId, | 普通文本Toast | -| showWarning | | required String? text, IconTextDirection direction, required BuildContext context, Duration duration, bool? preventTap, Color? backgroundColor, int? maxLines, TextStyle? textStyle, double? iconSize, Color? iconColor, String? toastId, | 警告Toast | +##### TToast.dismissAll + +关闭所有Toast + +返回类型:`void` + +##### TToast.dismissLoading + +关闭加载Toast(向后兼容) + +返回类型:`void` + +##### TToast.dismissToast + +关闭指定的Toast + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| toastId | String | - | - | + + +##### TToast.showFail + +失败提示Toast + +返回类型:`String` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| text | String? | - | - | +| direction | IconTextDirection | IconTextDirection.horizontal | - | +| context | BuildContext | - | - | +| duration | Duration | const Duration(milliseconds: 3000) | - | +| preventTap | bool? | - | - | +| backgroundColor | Color? | - | - | +| maxLines | int? | - | - | +| textStyle | TextStyle? | - | - | +| iconSize | double? | - | - | +| iconColor | Color? | - | - | +| toastId | String? | - | - | + + +##### TToast.showIconText + +带图标的Toast + +返回类型:`String` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| text | String? | - | - | +| icon | IconData? | - | - | +| direction | IconTextDirection | IconTextDirection.horizontal | - | +| context | BuildContext | - | - | +| duration | Duration | const Duration(milliseconds: 3000) | - | +| preventTap | bool? | - | - | +| backgroundColor | Color? | - | - | +| maxLines | int? | - | - | +| textStyle | TextStyle? | - | - | +| iconSize | double? | - | - | +| iconColor | Color? | - | - | +| toastId | String? | - | - | + + +##### TToast.showLoading + +带文案的加载Toast + +返回类型:`String` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | - | +| text | String? | - | - | +| duration | Duration | const Duration(seconds: 99999999) | - | +| preventTap | bool? | - | - | +| customWidget | Widget? | - | - | +| backgroundColor | Color? | - | - | +| textStyle | TextStyle? | - | - | +| iconSize | double? | - | - | +| iconColor | Color? | - | - | +| toastId | String? | - | - | + + +##### TToast.showLoadingWithoutText + +不带文案的加载Toast + +返回类型:`String` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | - | +| duration | Duration | const Duration(seconds: 99999999) | - | +| preventTap | bool? | - | - | +| backgroundColor | Color? | - | - | +| iconSize | double? | - | - | +| iconColor | Color? | - | - | +| toastId | String? | - | - | + + +##### TToast.showSuccess + +成功提示Toast + +返回类型:`String` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| text | String? | - | - | +| direction | IconTextDirection | IconTextDirection.horizontal | - | +| context | BuildContext | - | - | +| duration | Duration | const Duration(milliseconds: 3000) | - | +| preventTap | bool? | - | - | +| backgroundColor | Color? | - | - | +| maxLines | int? | - | - | +| textStyle | TextStyle? | - | - | +| iconSize | double? | - | - | +| iconColor | Color? | - | - | +| toastId | String? | - | - | + + +##### TToast.showText + +普通文本Toast + +返回类型:`String` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| text | String? | - | - | +| context | BuildContext | - | - | +| duration | Duration | const Duration(milliseconds: 3000) | - | +| maxLines | int? | - | - | +| constraints | BoxConstraints? | - | - | +| preventTap | bool? | - | - | +| customWidget | Widget? | - | - | +| backgroundColor | Color? | - | - | +| textStyle | TextStyle? | - | - | +| toastId | String? | - | - | + + +##### TToast.showWarning + +警告Toast + +返回类型:`String` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| text | String? | - | - | +| direction | IconTextDirection | IconTextDirection.horizontal | - | +| context | BuildContext | - | - | +| duration | Duration | const Duration(milliseconds: 3000) | - | +| preventTap | bool? | - | - | +| backgroundColor | Color? | - | - | +| maxLines | int? | - | - | +| textStyle | TextStyle? | - | - | +| iconSize | double? | - | - | +| iconColor | Color? | - | - | +| toastId | String? | - | - | + + +### IconTextDirection +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| horizontal | 横向 | +| vertical | 竖向 | diff --git a/tdesign-component/example/assets/api/tree-select_api.md b/tdesign-component/example/assets/api/tree-select_api.md index 7ac293da2..ddb46646a 100644 --- a/tdesign-component/example/assets/api/tree-select_api.md +++ b/tdesign-component/example/assets/api/tree-select_api.md @@ -6,15 +6,13 @@ | --- | --- | --- | --- | | defaultValue | List | const [] | 初始值,对应options中的value值 | | height | double | 336 | 高度 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | multiple | bool | false | 支持多选 | | onChange | TTreeSelectChangeEvent? | - | 选中值发生变化 | | options | List | const [] | 展示的选项列表 | | outwardCornerRadius | double | 9 | 一级菜单选中项的外弯折圆角半径,默认为 9 | | style | TTreeSelectStyle | TTreeSelectStyle.normal | 一级菜单样式 | -``` -``` ### TSelectOption #### 默认构造方法 @@ -27,3 +25,21 @@ | maxLines | int | 1 | 最大显示行数 | | multiple | bool | false | 当前子项支持多选 | | value | dynamic | - | 值 | + + +### TTreeSelectStyle +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| normal | - | +| outline | - | + + +### TTreeSelectChangeEvent +#### 类型定义 + +```dart +typedef TTreeSelectChangeEvent = void Function(List, int level); +``` diff --git a/tdesign-component/example/assets/api/upload_api.md b/tdesign-component/example/assets/api/upload_api.md index 978ac3fc3..52cd31835 100644 --- a/tdesign-component/example/assets/api/upload_api.md +++ b/tdesign-component/example/assets/api/upload_api.md @@ -8,7 +8,7 @@ | enabledReplaceType | bool? | false | 是否启用replace功能 | | files | List | - | 控制展示的文件列表 | | height | double? | 80.0 | 图片高度 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | max | int | 0 | 用于控制文件上传数量,0为不限制,仅在multiple为true时有效 | | mediaType | List | const [TUploadMediaType.image, TUploadMediaType.video] | 支持上传的文件类型,图片或视频 | | multiple | bool | false | 是否多选上传,默认false | @@ -25,3 +25,88 @@ | wrapAlignment | WrapAlignment? | - | 多图对齐方式 | | wrapRunSpacing | double? | - | 多图布局时的 runSpacing | | wrapSpacing | double? | - | 多图布局时的 spacing | + + +### TUploadMediaType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| image | - | +| video | - | + + +### TUploadValidatorError +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| overSize | - | +| overQuantity | - | + + +### TUploadFileStatus +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| success | - | +| loading | - | +| error | - | +| retry | - | + + +### TUploadType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| add | - | +| remove | - | +| replace | - | + + +### TUploadBoxType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| roundedSquare | - | +| circle | - | + + +### TUploadErrorEvent +#### 类型定义 + +```dart +typedef TUploadErrorEvent = void Function(Object e); +``` + + +### TUploadClickEvent +#### 类型定义 + +```dart +typedef TUploadClickEvent = void Function(int value); +``` + + +### TUploadValueChangedEvent +#### 类型定义 + +```dart +typedef TUploadValueChangedEvent = void Function(List files, TUploadType type); +``` + + +### TUploadValidatorEvent +#### 类型定义 + +```dart +typedef TUploadValidatorEvent = void Function(TUploadValidatorError e); +``` diff --git a/tdesign-component/example/assets/code/indexes._buildCustomIndexes.txt b/tdesign-component/example/assets/code/indexes._buildCustomIndexes.txt index afd087518..ffda89e81 100644 --- a/tdesign-component/example/assets/code/indexes._buildCustomIndexes.txt +++ b/tdesign-component/example/assets/code/indexes._buildCustomIndexes.txt @@ -9,16 +9,16 @@ Widget _buildCustomIndexes(BuildContext context) { theme: TButtonTheme.primary, type: TButtonType.outline, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.right, - modalTop: renderBox?.size.height, - builder: (context) { - return TIndexes( + TPopup.show( + context, + options: TPopupOptions.right( + width: 280, + inset: TPopupRightInset(top: renderBox?.size.height ?? 0), + child: TIndexes( indexList: indexList, builderIndex: (context, index, isActive) { return TText( - '自定义 ${index}', + '自定义 $index', textColor: isActive ? TTheme.of(context).brandNormalColor : TTheme.of(context).textColorPrimary, @@ -29,16 +29,10 @@ Widget _buildCustomIndexes(BuildContext context) { (element) => element['index'] == index)['children'] as List; return TCellGroup( - cells: list - .map((e) => TCell( - title: e, - )) - .toList(), + cells: list.map((e) => TCell(title: e)).toList(), ); }, - ); - }, - ), + )), ); }, ); diff --git a/tdesign-component/example/assets/code/indexes._buildOther.txt b/tdesign-component/example/assets/code/indexes._buildOther.txt index 64b0446c9..0af8b5ceb 100644 --- a/tdesign-component/example/assets/code/indexes._buildOther.txt +++ b/tdesign-component/example/assets/code/indexes._buildOther.txt @@ -9,12 +9,12 @@ Widget _buildOther(BuildContext context) { theme: TButtonTheme.primary, type: TButtonType.outline, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.right, - modalTop: renderBox?.size.height, - builder: (context) { - return TIndexes( + TPopup.show( + context, + options: TPopupOptions.right( + width: 280, + inset: TPopupRightInset(top: renderBox?.size.height ?? 0), + child: TIndexes( indexList: indexList, capsuleTheme: true, builderContent: (context, index) { @@ -22,16 +22,10 @@ Widget _buildOther(BuildContext context) { (element) => element['index'] == index)['children'] as List; return TCellGroup( - cells: list - .map((e) => TCell( - title: e, - )) - .toList(), + cells: list.map((e) => TCell(title: e)).toList(), ); }, - ); - }, - ), + )), ); }, ); diff --git a/tdesign-component/example/assets/code/indexes._buildSimple.txt b/tdesign-component/example/assets/code/indexes._buildSimple.txt index 5cec08001..93f3f3816 100644 --- a/tdesign-component/example/assets/code/indexes._buildSimple.txt +++ b/tdesign-component/example/assets/code/indexes._buildSimple.txt @@ -9,28 +9,22 @@ Widget _buildSimple(BuildContext context) { theme: TButtonTheme.primary, type: TButtonType.outline, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.right, - modalTop: renderBox?.size.height, - builder: (context) { - return TIndexes( + TPopup.show( + context, + options: TPopupOptions.right( + width: 280, + inset: TPopupRightInset(top: renderBox?.size.height ?? 0), + child: TIndexes( indexList: indexList, builderContent: (context, index) { final list = _list.firstWhere( (element) => element['index'] == index)['children'] as List; return TCellGroup( - cells: list - .map((e) => TCell( - title: e, - )) - .toList(), + cells: list.map((e) => TCell(title: e)).toList(), ); }, - ); - }, - ), + )), ); }, ); diff --git a/tdesign-component/example/assets/code/picker.buildCustomItemBuilder.txt b/tdesign-component/example/assets/code/picker.buildCustomItemBuilder.txt index c8c74463e..0e74fc62e 100644 --- a/tdesign-component/example/assets/code/picker.buildCustomItemBuilder.txt +++ b/tdesign-component/example/assets/code/picker.buildCustomItemBuilder.txt @@ -5,12 +5,14 @@ children: [ Text( '示例:itemBuilder 自定义子项渲染,可添加图标、背景色等', - style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder), + style: TextStyle( + fontSize: 12, color: TTheme.of(context).textColorPlaceholder), ), const SizedBox(height: 4), Text( '选中: ${_customItemBuilderValue.isEmpty ? "未选择" : _customItemBuilderValue}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary), + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary), ), const SizedBox(height: 8), _pickerCard( @@ -36,15 +38,19 @@ content, style: TextStyle( fontSize: 16, - fontWeight: selected ? FontWeight.w600 : FontWeight.normal, - color: selected ? theme.brandNormalColor : theme.fontGyColor1, + fontWeight: + selected ? FontWeight.w600 : FontWeight.normal, + color: selected + ? theme.brandNormalColor + : theme.fontGyColor1, ), ), ], ), ); }, - onChange: (v) => setState(() => _customItemBuilderValue = v.labels.first), + onChange: (v) => + setState(() => _customItemBuilderValue = v.labels.first), ), ), ], diff --git a/tdesign-component/example/assets/code/picker.buildCustomKeys.txt b/tdesign-component/example/assets/code/picker.buildCustomKeys.txt index 6c8f8dd8a..b7012897c 100644 --- a/tdesign-component/example/assets/code/picker.buildCustomKeys.txt +++ b/tdesign-component/example/assets/code/picker.buildCustomKeys.txt @@ -1,19 +1,22 @@ Widget buildCustomKeys(BuildContext context) { // 用 keys 告诉组件「city 映射为 label,code 是 value,readonly 是 disabled」 - const keys = TPickerKeys(label: 'city', value: 'code', disabled: 'readonly'); + const keys = + TPickerKeys(label: 'city', value: 'code', disabled: 'readonly'); final label = _customKeysValue?.labels.join() ?? ''; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '后端原始字段:city / code / readonly。通过 keys(label: "city") 映射为 label', - style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder), + style: TextStyle( + fontSize: 12, color: TTheme.of(context).textColorPlaceholder), ), const SizedBox(height: 4), Text( '当前选中:${label.isEmpty ? "未选择" : label}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary), + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary), ), const SizedBox(height: 8), _pickerCard( diff --git a/tdesign-component/example/assets/code/picker.buildCustomSize.txt b/tdesign-component/example/assets/code/picker.buildCustomSize.txt index f94fe3c70..b4606f643 100644 --- a/tdesign-component/example/assets/code/picker.buildCustomSize.txt +++ b/tdesign-component/example/assets/code/picker.buildCustomSize.txt @@ -5,7 +5,8 @@ children: [ Text( '示例:height(300) + itemCount(7),每屏显示 7 项', - style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder), + style: TextStyle( + fontSize: 12, color: TTheme.of(context).textColorPlaceholder), ), const SizedBox(height: 8), _pickerCard( diff --git a/tdesign-component/example/assets/code/picker.buildCustomSlot.txt b/tdesign-component/example/assets/code/picker.buildCustomSlot.txt index 4c7137928..d3f56da0e 100644 --- a/tdesign-component/example/assets/code/picker.buildCustomSlot.txt +++ b/tdesign-component/example/assets/code/picker.buildCustomSlot.txt @@ -17,8 +17,7 @@ titleWidget: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(TIcons.location, - size: 18, color: theme.brandNormalColor), + Icon(TIcons.location, size: 18, color: theme.brandNormalColor), const SizedBox(width: 4), Text('选择地区', style: TextStyle( @@ -29,7 +28,8 @@ ], ), cancel: Icon(TIcons.close, size: 22, color: theme.fontGyColor2), - confirm: Icon(TIcons.check, size: 22, color: theme.brandNormalColor), + confirm: + Icon(TIcons.check, size: 22, color: theme.brandNormalColor), ), ), ], diff --git a/tdesign-component/example/assets/code/picker.buildGlobalDisabled.txt b/tdesign-component/example/assets/code/picker.buildGlobalDisabled.txt index f7abd42cf..bb1e7e3df 100644 --- a/tdesign-component/example/assets/code/picker.buildGlobalDisabled.txt +++ b/tdesign-component/example/assets/code/picker.buildGlobalDisabled.txt @@ -21,13 +21,16 @@ const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: cityItems, initialValue: const ['GZ'], + child: TPicker( + items: cityItems, + initialValue: const ['GZ'], onChange: (v) => debugPrint('选中: $v'), disabled: globalDisabled), ), const SizedBox(height: 4), Text('切换开关可控制整个选择器的禁用/启用状态', - style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), + style: TextStyle( + fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), ], ); } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/picker.buildItemDisabled.txt b/tdesign-component/example/assets/code/picker.buildItemDisabled.txt index f7bf58114..7e80c7c81 100644 --- a/tdesign-component/example/assets/code/picker.buildItemDisabled.txt +++ b/tdesign-component/example/assets/code/picker.buildItemDisabled.txt @@ -3,15 +3,20 @@ return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('选中: ${selectedItemDisabled.isEmpty ? "未选择" : selectedItemDisabled}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + Text( + '选中: ${selectedItemDisabled.isEmpty ? "未选择" : selectedItemDisabled}', + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary)), const SizedBox(height: 4), Text('提示: 标灰的选项不可选(第1列「保密」、第2列「A排1座/A排6座/A排7座/A排8座/A排12座」)', - style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), + style: TextStyle( + fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: itemDisabledItems, initialValue: const ['M', 'A5'], + child: TPicker( + items: itemDisabledItems, + initialValue: const ['M', 'A5'], onChange: (v) => setState(() => selectedItemDisabled = '${v.labels.first} ${v.labels.last}')), ), diff --git a/tdesign-component/example/assets/code/picker.buildLinked.txt b/tdesign-component/example/assets/code/picker.buildLinked.txt index 90579f3c0..a3bfaea34 100644 --- a/tdesign-component/example/assets/code/picker.buildLinked.txt +++ b/tdesign-component/example/assets/code/picker.buildLinked.txt @@ -4,12 +4,16 @@ crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('选中地区: ${selectedLinked.isEmpty ? "未选择" : selectedLinked}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary)), const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: linkedItems, initialValue: const ['GD', 'SZ', 'NS'], - onChange: (v) => setState(() => selectedLinked = v.labels.join(' / '))), + child: TPicker( + items: linkedItems, + initialValue: const ['GD', 'SZ', 'NS'], + onChange: (v) => + setState(() => selectedLinked = v.labels.join(' / '))), ), ], ); diff --git a/tdesign-component/example/assets/code/picker.buildSingleColumn.txt b/tdesign-component/example/assets/code/picker.buildSingleColumn.txt index 839df74f9..953af8c2a 100644 --- a/tdesign-component/example/assets/code/picker.buildSingleColumn.txt +++ b/tdesign-component/example/assets/code/picker.buildSingleColumn.txt @@ -4,11 +4,13 @@ crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('选中城市: ${selectedCity.isEmpty ? "未选择" : selectedCity}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary)), const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: cityItems, + child: TPicker( + items: cityItems, onChange: (v) => setState(() => selectedCity = v.labels.first)), ), ], diff --git a/tdesign-component/example/assets/code/picker.buildTimeSelect.txt b/tdesign-component/example/assets/code/picker.buildTimeSelect.txt index eb85ddf1f..f9c5f818f 100644 --- a/tdesign-component/example/assets/code/picker.buildTimeSelect.txt +++ b/tdesign-component/example/assets/code/picker.buildTimeSelect.txt @@ -4,13 +4,16 @@ crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('选中时间: ${selectedTime.isEmpty ? "未选择" : selectedTime}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary)), const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: timeItems, itemCount: 5, - onChange: (v) => setState(() => - selectedTime = '${v.values[0]}:${v.values[1].toString().padLeft(2, '0')}:${v.values[2].toString().padLeft(2, '0')}')), + child: TPicker( + items: timeItems, + itemCount: 5, + onChange: (v) => setState(() => selectedTime = + '${v.values[0]}:${v.values[1].toString().padLeft(2, '0')}:${v.values[2].toString().padLeft(2, '0')}')), ), ], ); diff --git a/tdesign-component/example/assets/code/popup._buildApiDuration.txt b/tdesign-component/example/assets/code/popup._buildApiDuration.txt new file mode 100644 index 000000000..eeb7fc98c --- /dev/null +++ b/tdesign-component/example/assets/code/popup._buildApiDuration.txt @@ -0,0 +1,22 @@ + + Widget _buildApiDuration(BuildContext context) { + return TButton( + text: 'animationDuration: 600ms', + isBlock: true, + theme: TButtonTheme.primary, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () { + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 240, + animationDuration: const Duration(milliseconds: 600), + child: Container( + height: 200, + color: TTheme.of(context).bgColorContainer, + )), + ); + }, + ); + } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/popup._buildApiInset.txt b/tdesign-component/example/assets/code/popup._buildApiInset.txt new file mode 100644 index 000000000..03f9ad0a0 --- /dev/null +++ b/tdesign-component/example/assets/code/popup._buildApiInset.txt @@ -0,0 +1,23 @@ + + Widget _buildApiInset(BuildContext context) { + return TButton( + text: 'bottom inset.left/right', + isBlock: true, + theme: TButtonTheme.primary, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () { + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 320, + inset: const TPopupBottomInset(left: 16, right: 16), + titleWidget: TText('左右留白'), + child: Container( + height: 240, + color: TTheme.of(context).bgColorContainer, + )), + ); + }, + ); + } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/popup._buildApiOnOverlayClick.txt b/tdesign-component/example/assets/code/popup._buildApiOnOverlayClick.txt new file mode 100644 index 000000000..d24828ea7 --- /dev/null +++ b/tdesign-component/example/assets/code/popup._buildApiOnOverlayClick.txt @@ -0,0 +1,22 @@ + + Widget _buildApiOnOverlayClick(BuildContext context) { + return TButton( + text: 'onOverlayClick', + isBlock: true, + theme: TButtonTheme.primary, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () { + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 260, + onOverlayClick: () => TToast.showText('点击蒙层', context: context), + child: Container( + height: 200, + color: TTheme.of(context).bgColorContainer, + )), + ); + }, + ); + } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/popup._buildApiShowOverlayFalse.txt b/tdesign-component/example/assets/code/popup._buildApiShowOverlayFalse.txt new file mode 100644 index 000000000..58776fda0 --- /dev/null +++ b/tdesign-component/example/assets/code/popup._buildApiShowOverlayFalse.txt @@ -0,0 +1,25 @@ + + Widget _buildApiShowOverlayFalse(BuildContext context) { + return TButton( + text: 'showOverlay: false(透明模态)', + isBlock: true, + theme: TButtonTheme.primary, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () { + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 280, + showOverlay: false, + modal: true, + // 不显示可见蒙层,但仍阻断背景交互;须保留其它关闭入口。 + titleWidget: const TText('透明模态'), + child: Container( + height: 200, + color: TTheme.of(context).bgColorContainer, + )), + ); + }, + ); + } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/popup._buildNestedPopup.txt b/tdesign-component/example/assets/code/popup._buildNestedPopup.txt new file mode 100644 index 000000000..2ffd8f295 --- /dev/null +++ b/tdesign-component/example/assets/code/popup._buildNestedPopup.txt @@ -0,0 +1,64 @@ + + Widget _buildNestedPopup(BuildContext context) { + return TButton( + text: '内层再弹一层(嵌套叠加)', + isBlock: true, + theme: TButtonTheme.primary, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () { + TPopupHandle? outerHandle; + outerHandle = TPopup.show( + context, + options: TPopupOptions.bottom( + height: 360, + headerBuilder: null, + child: Builder( + builder: (innerContext) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TText( + '外层:headerBuilder: null,仅 child', + textColor: TTheme.of(innerContext).textColorSecondary, + ), + const SizedBox(height: 16), + TButton( + text: '打开内层 Popup', + isBlock: true, + theme: TButtonTheme.primary, + size: TButtonSize.large, + onTap: () { + TPopup.show( + innerContext, + options: TPopupOptions.bottom( + height: 280, + titleWidget: const TText('内层标题'), + child: Container( + height: 160, + color: TTheme.of(innerContext) + .bgColorSecondaryContainer, + ), + ), + ); + }, + ), + const SizedBox(height: 12), + TButton( + text: '关闭外层', + isBlock: true, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () => outerHandle?.close(), + ), + ], + ), + ); + }, + )), + ); + }, + ); + } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/popup._buildPopFromBottom.txt b/tdesign-component/example/assets/code/popup._buildPopFromBottom.txt index 39ec6d336..d10c8b269 100644 --- a/tdesign-component/example/assets/code/popup._buildPopFromBottom.txt +++ b/tdesign-component/example/assets/code/popup._buildPopFromBottom.txt @@ -7,15 +7,15 @@ type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return Container( - color: TTheme.of(context).bgColorContainer, - height: 240, - ); - }), + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 240, + headerBuilder: null, + child: Container( + color: TTheme.of(context).bgColorContainer, + height: 240, + )), ); }, ); diff --git a/tdesign-component/example/assets/code/popup._buildPopFromBottomWithClose.txt b/tdesign-component/example/assets/code/popup._buildPopFromBottomWithClose.txt deleted file mode 100644 index 35edc505c..000000000 --- a/tdesign-component/example/assets/code/popup._buildPopFromBottomWithClose.txt +++ /dev/null @@ -1,24 +0,0 @@ - - Widget _buildPopFromBottomWithClose(BuildContext context) { - return TButton( - text: '底部弹出层-带关闭', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomDisplayPanel( - closeClick: () { - Navigator.maybePop(context); - }, - child: Container(height: 200), - ); - }), - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/popup._buildPopFromBottomWithCloseAndLeftTitle.txt b/tdesign-component/example/assets/code/popup._buildPopFromBottomWithCloseAndLeftTitle.txt deleted file mode 100644 index bdc0fdf7c..000000000 --- a/tdesign-component/example/assets/code/popup._buildPopFromBottomWithCloseAndLeftTitle.txt +++ /dev/null @@ -1,26 +0,0 @@ - - Widget _buildPopFromBottomWithCloseAndLeftTitle(BuildContext context) { - return TButton( - text: '底部弹出层-带左边标题及关闭', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomDisplayPanel( - title: '标题文字', - titleLeft: true, - closeClick: () { - Navigator.maybePop(context); - }, - child: Container(height: 200), - ); - }), - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/popup._buildPopFromBottomWithCloseAndTitle.txt b/tdesign-component/example/assets/code/popup._buildPopFromBottomWithCloseAndTitle.txt index a8eba6f9c..83ddf96b2 100644 --- a/tdesign-component/example/assets/code/popup._buildPopFromBottomWithCloseAndTitle.txt +++ b/tdesign-component/example/assets/code/popup._buildPopFromBottomWithCloseAndTitle.txt @@ -7,18 +7,49 @@ type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomDisplayPanel( - title: '标题文字', - closeClick: () { - Navigator.maybePop(context); - }, - child: Container(height: 200), - ); - }), + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 280, + cancelBuilder: (_, close) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: close, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: TText( + '关闭', + textColor: TTheme.of(context).textColorSecondary, + font: TTheme.of(context).fontBodyLarge, + ), + ), + ), + titleWidget: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(TIcons.info_circle, + color: TTheme.of(context).brandNormalColor, size: 18), + const SizedBox(width: 4), + TText( + '自定义标题', + textColor: TTheme.of(context).brandNormalColor, + font: TTheme.of(context).fontTitleMedium, + ), + ], + ), + confirmBuilder: (_, close) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: close, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: TText( + '完成', + textColor: TTheme.of(context).brandNormalColor, + font: TTheme.of(context).fontTitleMedium, + fontWeight: FontWeight.w600, + ), + ), + ), + child: Container(height: 200)), ); }, ); diff --git a/tdesign-component/example/assets/code/popup._buildPopFromBottomWithOperation.txt b/tdesign-component/example/assets/code/popup._buildPopFromBottomWithOperation.txt deleted file mode 100644 index 9d37be37f..000000000 --- a/tdesign-component/example/assets/code/popup._buildPopFromBottomWithOperation.txt +++ /dev/null @@ -1,29 +0,0 @@ - - Widget _buildPopFromBottomWithOperation(BuildContext context) { - return TButton( - text: '底部弹出层-带操作', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push(TSlidePopupRoute( - modalBarrierColor: TTheme.of(context).fontGyColor2, - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomConfirmPanel( - leftClick: () { - Navigator.maybePop(context); - }, - rightClick: () { - TToast.showText('确定', context: context); - Navigator.maybePop(context); - }, - child: Container( - height: 200, - ), - ); - })); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/popup._buildPopFromBottomWithOperationAndTitle.txt b/tdesign-component/example/assets/code/popup._buildPopFromBottomWithOperationAndTitle.txt index dee0cb931..88e0ea6f0 100644 --- a/tdesign-component/example/assets/code/popup._buildPopFromBottomWithOperationAndTitle.txt +++ b/tdesign-component/example/assets/code/popup._buildPopFromBottomWithOperationAndTitle.txt @@ -7,23 +7,12 @@ type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomConfirmPanel( - title: '标题文字', - leftClick: () { - Navigator.maybePop(context); - }, - rightClick: () { - TToast.showText('确定', context: context); - Navigator.maybePop(context); - }, - child: Container(height: 200), - ); - }, - ), + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 280, + titleWidget: TText('标题文字'), + child: Container(height: 200)), ); }, ); diff --git a/tdesign-component/example/assets/code/popup._buildPopFromBottomWithTitle.txt b/tdesign-component/example/assets/code/popup._buildPopFromBottomWithTitle.txt deleted file mode 100644 index e88aae7e1..000000000 --- a/tdesign-component/example/assets/code/popup._buildPopFromBottomWithTitle.txt +++ /dev/null @@ -1,26 +0,0 @@ - - Widget _buildPopFromBottomWithTitle(BuildContext context) { - return TButton( - text: '底部弹出层-仅标题', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomDisplayPanel( - title: '标题文字', - hideClose: true, - // closeClick: () { - // Navigator.maybePop(context); - // }, - child: Container(height: 200), - ); - }), - ); - }, - ); - } \ No newline at end of file diff --git a/tdesign-component/example/assets/code/popup._buildPopFromCenter.txt b/tdesign-component/example/assets/code/popup._buildPopFromCenter.txt index 2262272ba..1d4511bca 100644 --- a/tdesign-component/example/assets/code/popup._buildPopFromCenter.txt +++ b/tdesign-component/example/assets/code/popup._buildPopFromCenter.txt @@ -7,20 +7,19 @@ type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.center, - builder: (context) { - return Container( - decoration: BoxDecoration( - color: TTheme.of(context).bgColorContainer, - borderRadius: - BorderRadius.circular(TTheme.of(context).radiusLarge), - ), - width: 240, - height: 240, - ); - }), + TPopup.show( + context, + options: TPopupOptions.center( + closeBuilder: null, + child: Container( + decoration: BoxDecoration( + color: TTheme.of(context).bgColorContainer, + borderRadius: + BorderRadius.circular(TTheme.of(context).radiusLarge), + ), + width: 240, + height: 240, + )), ); }, ); diff --git a/tdesign-component/example/assets/code/popup._buildPopFromCenterWithClose.txt b/tdesign-component/example/assets/code/popup._buildPopFromCenterWithClose.txt index 08f3aa831..376499c95 100644 --- a/tdesign-component/example/assets/code/popup._buildPopFromCenterWithClose.txt +++ b/tdesign-component/example/assets/code/popup._buildPopFromCenterWithClose.txt @@ -7,18 +7,21 @@ type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - isDismissible: false, - slideTransitionFrom: SlideTransitionFrom.center, - builder: (context) { - return TPopupCenterPanel( - closeClick: () { - Navigator.maybePop(context); - }, - child: const SizedBox(width: 240, height: 240), - ); - }), + TPopup.show( + context, + options: TPopupOptions.center( + closeOnOverlayClick: false, + width: 240, + height: 240, + closeBuilder: (_, close) => IconButton( + icon: Icon( + TIcons.close_circle, + color: TTheme.of(context).fontWhColor1, + size: 32, + ), + onPressed: close, + ), + child: const SizedBox(width: 240, height: 240)), ); }, ); diff --git a/tdesign-component/example/assets/code/popup._buildPopFromCenterWithUnderClose.txt b/tdesign-component/example/assets/code/popup._buildPopFromCenterWithUnderClose.txt index e22ac50fe..b7dec4b2f 100644 --- a/tdesign-component/example/assets/code/popup._buildPopFromCenterWithUnderClose.txt +++ b/tdesign-component/example/assets/code/popup._buildPopFromCenterWithUnderClose.txt @@ -1,25 +1,31 @@ Widget _buildPopFromCenterWithUnderClose(BuildContext context) { return TButton( - text: '居中弹出层-关闭在下方', + text: '居中弹出层-自定义下方按钮', isBlock: true, theme: TButtonTheme.primary, type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - isDismissible: false, - slideTransitionFrom: SlideTransitionFrom.center, - builder: (context) { - return TPopupCenterPanel( - closeUnderBottom: true, - closeClick: () { - Navigator.maybePop(context); - }, - child: const SizedBox(width: 240, height: 240), - ); - }), + TPopup.show( + context, + options: TPopupOptions.center( + closeOnOverlayClick: true, + width: 240, + height: 200, + closeBuilder: (_, close) => IconButton( + icon: Icon( + TIcons.poweroff, + color: TTheme.of(context).fontWhColor1, + size: 36, + ), + onPressed: close, + ), + child: Container( + width: 240, + height: 200, + color: TTheme.of(context).bgColorContainer, + )), ); }, ); diff --git a/tdesign-component/example/assets/code/popup._buildPopFromLeft.txt b/tdesign-component/example/assets/code/popup._buildPopFromLeft.txt index fba388fa5..91184529b 100644 --- a/tdesign-component/example/assets/code/popup._buildPopFromLeft.txt +++ b/tdesign-component/example/assets/code/popup._buildPopFromLeft.txt @@ -7,15 +7,13 @@ type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.left, - builder: (context) { - return Container( - color: TTheme.of(context).bgColorContainer, - width: 280, - ); - }), + TPopup.show( + context, + options: TPopupOptions.left( + width: 280, + child: Container( + color: TTheme.of(context).bgColorContainer, + )), ); }, ); diff --git a/tdesign-component/example/assets/code/popup._buildPopFromRight.txt b/tdesign-component/example/assets/code/popup._buildPopFromRight.txt index 650e05195..3d06a9c16 100644 --- a/tdesign-component/example/assets/code/popup._buildPopFromRight.txt +++ b/tdesign-component/example/assets/code/popup._buildPopFromRight.txt @@ -7,15 +7,13 @@ type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.right, - builder: (context) { - return Container( - color: TTheme.of(context).bgColorContainer, - width: 280, - ); - }), + TPopup.show( + context, + options: TPopupOptions.right( + width: 280, + child: Container( + color: TTheme.of(context).bgColorContainer, + )), ); }, ); diff --git a/tdesign-component/example/assets/code/popup._buildPopFromTop.txt b/tdesign-component/example/assets/code/popup._buildPopFromTop.txt index f0675a65a..112a0abf4 100644 --- a/tdesign-component/example/assets/code/popup._buildPopFromTop.txt +++ b/tdesign-component/example/assets/code/popup._buildPopFromTop.txt @@ -7,21 +7,16 @@ type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.top, - open: () { - print('open'); - }, - opened: () { - print('opened'); - }, - builder: (context) { - return Container( - color: TTheme.of(context).bgColorContainer, - height: 240, - ); - }), + TPopup.show( + context, + options: TPopupOptions.top( + height: 240, + onOpen: () => print('open'), + onOpened: () => print('opened'), + child: Container( + color: TTheme.of(context).bgColorContainer, + height: 240, + )), ); }, ); diff --git a/tdesign-component/example/lib/component_test/popup_test.dart b/tdesign-component/example/lib/component_test/popup_test.dart index 7aeb93e9d..b0f9bea3c 100644 --- a/tdesign-component/example/lib/component_test/popup_test.dart +++ b/tdesign-component/example/lib/component_test/popup_test.dart @@ -9,7 +9,7 @@ class ConfirmDialogTestApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'confirmDialog 测试示例', + title: 'TPopup 测试示例', theme: ThemeData(primarySwatch: Colors.blue), home: const TestPage(), ); @@ -24,43 +24,29 @@ class TestPage extends StatefulWidget { } class _TestPageState extends State { - final TextEditingController _searchNameController = TextEditingController(); - final TextEditingController _searchRemarkController = TextEditingController(); - void _showProblemDialog() { - - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomDisplayPanel( - title: 'title', - radius: 20, - backgroundColor: const Color(0xFFFAFFFC), - closeClick: () { - Navigator.maybePop(context); - }, - child: Container( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 33), - decoration: const BoxDecoration(color: Colors.white), - child: const Column( - children: [ - Center( - child: Text("立即拨打"), - ), - ], - ), + TPopup.show( + context, + options: TPopupOptions.bottom( + titleWidget: TText('title'), + radius: 20, + backgroundColor: const Color(0xFFFAFFFC), + child: Container( + padding: const EdgeInsets.only(left: 20, right: 20, bottom: 33), + decoration: const BoxDecoration(color: Colors.white), + child: const Column( + children: [ + Center(child: Text('立即拨打')), + ], ), - ); - }, - ), + )), ); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('TConfirmDialog测试')), + appBar: AppBar(title: const Text('TPopup测试')), body: Center( child: TButton( child: const Text('显示问题弹窗'), @@ -69,11 +55,4 @@ class _TestPageState extends State { ), ); } - - @override - void dispose() { - _searchNameController.dispose(); - _searchRemarkController.dispose(); - super.dispose(); - } -} \ No newline at end of file +} diff --git a/tdesign-component/example/lib/page/t_indexes_page.dart b/tdesign-component/example/lib/page/t_indexes_page.dart index caf9b55e9..d0b1f5cf2 100644 --- a/tdesign-component/example/lib/page/t_indexes_page.dart +++ b/tdesign-component/example/lib/page/t_indexes_page.dart @@ -157,28 +157,22 @@ Widget _buildSimple(BuildContext context) { theme: TButtonTheme.primary, type: TButtonType.outline, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.right, - modalTop: renderBox?.size.height, - builder: (context) { - return TIndexes( + TPopup.show( + context, + options: TPopupOptions.right( + width: 280, + inset: TPopupRightInset(top: renderBox?.size.height ?? 0), + child: TIndexes( indexList: indexList, builderContent: (context, index) { final list = _list.firstWhere( (element) => element['index'] == index)['children'] as List; return TCellGroup( - cells: list - .map((e) => TCell( - title: e, - )) - .toList(), + cells: list.map((e) => TCell(title: e)).toList(), ); }, - ); - }, - ), + )), ); }, ); @@ -195,12 +189,12 @@ Widget _buildOther(BuildContext context) { theme: TButtonTheme.primary, type: TButtonType.outline, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.right, - modalTop: renderBox?.size.height, - builder: (context) { - return TIndexes( + TPopup.show( + context, + options: TPopupOptions.right( + width: 280, + inset: TPopupRightInset(top: renderBox?.size.height ?? 0), + child: TIndexes( indexList: indexList, capsuleTheme: true, builderContent: (context, index) { @@ -208,16 +202,10 @@ Widget _buildOther(BuildContext context) { (element) => element['index'] == index)['children'] as List; return TCellGroup( - cells: list - .map((e) => TCell( - title: e, - )) - .toList(), + cells: list.map((e) => TCell(title: e)).toList(), ); }, - ); - }, - ), + )), ); }, ); @@ -234,16 +222,16 @@ Widget _buildCustomIndexes(BuildContext context) { theme: TButtonTheme.primary, type: TButtonType.outline, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.right, - modalTop: renderBox?.size.height, - builder: (context) { - return TIndexes( + TPopup.show( + context, + options: TPopupOptions.right( + width: 280, + inset: TPopupRightInset(top: renderBox?.size.height ?? 0), + child: TIndexes( indexList: indexList, builderIndex: (context, index, isActive) { return TText( - '自定义 ${index}', + '自定义 $index', textColor: isActive ? TTheme.of(context).brandNormalColor : TTheme.of(context).textColorPrimary, @@ -254,16 +242,10 @@ Widget _buildCustomIndexes(BuildContext context) { (element) => element['index'] == index)['children'] as List; return TCellGroup( - cells: list - .map((e) => TCell( - title: e, - )) - .toList(), + cells: list.map((e) => TCell(title: e)).toList(), ); }, - ); - }, - ), + )), ); }, ); diff --git a/tdesign-component/example/lib/page/t_picker_page.dart b/tdesign-component/example/lib/page/t_picker_page.dart index 744b06aa0..1b548b6f5 100644 --- a/tdesign-component/example/lib/page/t_picker_page.dart +++ b/tdesign-component/example/lib/page/t_picker_page.dart @@ -33,13 +33,16 @@ class _TPickerPageState extends State { final timeItems = TPickerColumns([ [ - for (int i = 0; i < 24; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}时', value: i), + for (int i = 0; i < 24; i++) + TPickerOption(label: '${i.toString().padLeft(2, '0')}时', value: i), ], [ - for (int i = 0; i < 60; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}分', value: i), + for (int i = 0; i < 60; i++) + TPickerOption(label: '${i.toString().padLeft(2, '0')}分', value: i), ], [ - for (int i = 0; i < 60; i++) TPickerOption(label: '${i.toString().padLeft(2, '0')}秒', value: i), + for (int i = 0; i < 60; i++) + TPickerOption(label: '${i.toString().padLeft(2, '0')}秒', value: i), ], ]); @@ -101,18 +104,18 @@ class _TPickerPageState extends State { ], // 第2列:开头 + 中间 + 结尾 各 1 个禁用(稀疏分布,留足操作空间) [ - TPickerOption(label: 'A排1座', value: 'A1', disabled: true), // 开头禁用 + TPickerOption(label: 'A排1座', value: 'A1', disabled: true), // 开头禁用 TPickerOption(label: 'A排2座', value: 'A2'), TPickerOption(label: 'A排3座', value: 'A3'), TPickerOption(label: 'A排4座', value: 'A4'), TPickerOption(label: 'A排5座', value: 'A5'), TPickerOption(label: 'A排6座', value: 'A6', disabled: true), - TPickerOption(label: 'A排7座', value: 'A7', disabled: true), // 中间偏后禁用 - TPickerOption(label: 'A排8座', value: 'A8', disabled: true), // 新增禁用 + TPickerOption(label: 'A排7座', value: 'A7', disabled: true), // 中间偏后禁用 + TPickerOption(label: 'A排8座', value: 'A8', disabled: true), // 新增禁用 TPickerOption(label: 'A排9座', value: 'A9'), TPickerOption(label: 'A排10座', value: 'A10'), TPickerOption(label: 'A排11座', value: 'A11'), - TPickerOption(label: 'A排12座', value: 'A12', disabled: true), // 结尾禁用 + TPickerOption(label: 'A排12座', value: 'A12', disabled: true), // 结尾禁用 ], ]); @@ -157,7 +160,8 @@ class _TPickerPageState extends State { ]), ExampleModule(title: '禁用状态', children: [ ExampleItem(desc: '项级 disabled(部分选项不可选)', builder: buildItemDisabled), - ExampleItem(desc: '全局 disabled(整组不可操作)', builder: buildGlobalDisabled), + ExampleItem( + desc: '全局 disabled(整组不可操作)', builder: buildGlobalDisabled), ]), ExampleModule(title: '弹窗模式(TPopup)', children: [ ExampleItem(desc: '弹窗-联动选择(省市区)', builder: buildPopupLinked), @@ -167,11 +171,13 @@ class _TPickerPageState extends State { ExampleItem(desc: '自定义按钮(图标 / 文字)', builder: buildCustomSlot), ]), ExampleModule(title: '自定义字段映射(keys)', children: [ - ExampleItem(desc: '数据字段非 label/value 时,用 keys 映射', builder: buildCustomKeys), + ExampleItem( + desc: '数据字段非 label/value 时,用 keys 映射', builder: buildCustomKeys), ]), ExampleModule(title: '尺寸与样式', children: [ ExampleItem(desc: '自定义高度和每屏显示数量', builder: buildCustomSize), - ExampleItem(desc: '自定义子项渲染(itemBuilder)', builder: buildCustomItemBuilder), + ExampleItem( + desc: '自定义子项渲染(itemBuilder)', builder: buildCustomItemBuilder), ]), ], ); @@ -184,17 +190,18 @@ class _TPickerPageState extends State { /// TPicker 自带「取消 / 标题 / 确认」工具栏,业务方在 onCancel/onConfirm /// 中自行决定是否调用 Navigator.pop 关闭弹窗。 void _showPickerPopup(BuildContext context, {required Widget picker}) { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (ctx) => Material( - color: TTheme.of(ctx).bgColorContainer, - child: SafeArea( - top: false, - child: picker, - ), - ), - ), + TPopup.show( + context, + options: TPopupOptions.bottom( + cancelBuilder: null, + confirmBuilder: null, + child: Material( + color: TTheme.of(context).bgColorContainer, + child: SafeArea( + top: false, + child: picker, + ), + )), ); } @@ -219,11 +226,13 @@ class _TPickerPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('选中城市: ${selectedCity.isEmpty ? "未选择" : selectedCity}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary)), const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: cityItems, + child: TPicker( + items: cityItems, onChange: (v) => setState(() => selectedCity = v.labels.first)), ), ], @@ -236,13 +245,16 @@ class _TPickerPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('选中时间: ${selectedTime.isEmpty ? "未选择" : selectedTime}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary)), const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: timeItems, itemCount: 5, - onChange: (v) => setState(() => - selectedTime = '${v.values[0]}:${v.values[1].toString().padLeft(2, '0')}:${v.values[2].toString().padLeft(2, '0')}')), + child: TPicker( + items: timeItems, + itemCount: 5, + onChange: (v) => setState(() => selectedTime = + '${v.values[0]}:${v.values[1].toString().padLeft(2, '0')}:${v.values[2].toString().padLeft(2, '0')}')), ), ], ); @@ -254,12 +266,16 @@ class _TPickerPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('选中地区: ${selectedLinked.isEmpty ? "未选择" : selectedLinked}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary)), const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: linkedItems, initialValue: const ['GD', 'SZ', 'NS'], - onChange: (v) => setState(() => selectedLinked = v.labels.join(' / '))), + child: TPicker( + items: linkedItems, + initialValue: const ['GD', 'SZ', 'NS'], + onChange: (v) => + setState(() => selectedLinked = v.labels.join(' / '))), ), ], ); @@ -272,15 +288,20 @@ class _TPickerPageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('选中: ${selectedItemDisabled.isEmpty ? "未选择" : selectedItemDisabled}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + Text( + '选中: ${selectedItemDisabled.isEmpty ? "未选择" : selectedItemDisabled}', + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary)), const SizedBox(height: 4), Text('提示: 标灰的选项不可选(第1列「保密」、第2列「A排1座/A排6座/A排7座/A排8座/A排12座」)', - style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), + style: TextStyle( + fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: itemDisabledItems, initialValue: const ['M', 'A5'], + child: TPicker( + items: itemDisabledItems, + initialValue: const ['M', 'A5'], onChange: (v) => setState(() => selectedItemDisabled = '${v.labels.first} ${v.labels.last}')), ), @@ -311,13 +332,16 @@ class _TPickerPageState extends State { const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: cityItems, initialValue: const ['GZ'], + child: TPicker( + items: cityItems, + initialValue: const ['GZ'], onChange: (v) => debugPrint('选中: $v'), disabled: globalDisabled), ), const SizedBox(height: 4), Text('切换开关可控制整个选择器的禁用/启用状态', - style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), + style: TextStyle( + fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), ], ); } @@ -372,8 +396,7 @@ class _TPickerPageState extends State { titleWidget: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(TIcons.location, - size: 18, color: theme.brandNormalColor), + Icon(TIcons.location, size: 18, color: theme.brandNormalColor), const SizedBox(width: 4), Text('选择地区', style: TextStyle( @@ -384,7 +407,8 @@ class _TPickerPageState extends State { ], ), cancel: Icon(TIcons.close, size: 22, color: theme.fontGyColor2), - confirm: Icon(TIcons.check, size: 22, color: theme.brandNormalColor), + confirm: + Icon(TIcons.check, size: 22, color: theme.brandNormalColor), ), ), ], @@ -502,7 +526,7 @@ class _TPickerPageState extends State { {'code': 'BJ', 'city': '北京', 'readonly': false}, {'code': 'SH', 'city': '上海', 'readonly': false}, {'code': 'GZ', 'city': '广州', 'readonly': false}, - {'code': 'SZ', 'city': '深圳', 'readonly': true}, // 演示禁用映射 + {'code': 'SZ', 'city': '深圳', 'readonly': true}, // 演示禁用映射 {'code': 'CD', 'city': '成都', 'readonly': false}, {'code': 'HZ', 'city': '杭州', 'readonly': false}, ], @@ -515,19 +539,22 @@ class _TPickerPageState extends State { @Demo(group: 'picker') Widget buildCustomKeys(BuildContext context) { // 用 keys 告诉组件「city 映射为 label,code 是 value,readonly 是 disabled」 - const keys = TPickerKeys(label: 'city', value: 'code', disabled: 'readonly'); + const keys = + TPickerKeys(label: 'city', value: 'code', disabled: 'readonly'); final label = _customKeysValue?.labels.join() ?? ''; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '后端原始字段:city / code / readonly。通过 keys(label: "city") 映射为 label', - style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder), + style: TextStyle( + fontSize: 12, color: TTheme.of(context).textColorPlaceholder), ), const SizedBox(height: 4), Text( '当前选中:${label.isEmpty ? "未选择" : label}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary), + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary), ), const SizedBox(height: 8), _pickerCard( @@ -551,7 +578,8 @@ class _TPickerPageState extends State { children: [ Text( '示例:height(300) + itemCount(7),每屏显示 7 项', - style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder), + style: TextStyle( + fontSize: 12, color: TTheme.of(context).textColorPlaceholder), ), const SizedBox(height: 8), _pickerCard( @@ -576,12 +604,14 @@ class _TPickerPageState extends State { children: [ Text( '示例:itemBuilder 自定义子项渲染,可添加图标、背景色等', - style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder), + style: TextStyle( + fontSize: 12, color: TTheme.of(context).textColorPlaceholder), ), const SizedBox(height: 4), Text( '选中: ${_customItemBuilderValue.isEmpty ? "未选择" : _customItemBuilderValue}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary), + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary), ), const SizedBox(height: 8), _pickerCard( @@ -607,15 +637,19 @@ class _TPickerPageState extends State { content, style: TextStyle( fontSize: 16, - fontWeight: selected ? FontWeight.w600 : FontWeight.normal, - color: selected ? theme.brandNormalColor : theme.fontGyColor1, + fontWeight: + selected ? FontWeight.w600 : FontWeight.normal, + color: selected + ? theme.brandNormalColor + : theme.fontGyColor1, ), ), ], ), ); }, - onChange: (v) => setState(() => _customItemBuilderValue = v.labels.first), + onChange: (v) => + setState(() => _customItemBuilderValue = v.labels.first), ), ), ], diff --git a/tdesign-component/example/lib/page/t_popup_page.dart b/tdesign-component/example/lib/page/t_popup_page.dart index 892855a8c..898800b37 100644 --- a/tdesign-component/example/lib/page/t_popup_page.dart +++ b/tdesign-component/example/lib/page/t_popup_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; + import '../annotation/demo.dart'; import '../base/example_widget.dart'; @@ -9,6 +10,44 @@ import '../base/example_widget.dart'; class TPopupPage extends StatelessWidget { const TPopupPage({super.key}); + static const double _headerHeight = 58; + + /// 底部标题 + 关闭(自定义 headerBuilder:标题居中 + 右侧关闭图标)。 + static TPopupHeaderBuilder _bottomTitleCloseHeader({ + String? title, + }) { + return (BuildContext ctx, VoidCallback close) { + final theme = TTheme.of(ctx); + final headerTitle = (title != null && title.isNotEmpty) + ? TText( + title, + textColor: theme.textColorPrimary, + font: theme.fontTitleLarge, + fontWeight: FontWeight.w700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + : null; + return SizedBox( + height: _headerHeight, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: [ + Expanded( + child: Center(child: headerTitle ?? const SizedBox.shrink()), + ), + IconButton( + icon: Icon(TIcons.close, color: theme.textColorSecondary), + onPressed: close, + ), + ], + ), + ), + ); + }; + } + @override Widget build(BuildContext context) { return ExamplePage( @@ -32,438 +71,242 @@ class TPopupPage extends StatelessWidget { title: '组件示例', children: [ ExampleItem(builder: _buildPopFromBottomWithOperationAndTitle), - ExampleItem(builder: _buildPopFromBottomWithOperation), ExampleItem(builder: _buildPopFromBottomWithCloseAndTitle), - ExampleItem(builder: _buildPopFromBottomWithCloseAndLeftTitle), - ExampleItem(builder: _buildPopFromBottomWithClose), - ExampleItem(builder: _buildPopFromBottomWithTitle), ExampleItem(builder: _buildPopFromCenterWithClose), ExampleItem(builder: _buildPopFromCenterWithUnderClose), + ExampleItem(builder: _buildNestedPopup), + ], + ), + ExampleModule( + title: '更多 API', + children: [ + ExampleItem(builder: _buildApiInset), + ExampleItem(builder: _buildApiShowOverlayFalse), + ExampleItem(builder: _buildApiOnOverlayClick), + ExampleItem(builder: _buildApiDuration), ], ), ], test: [ ExampleItem( - desc: '操作栏超长文本,指定颜色', - builder: (_) { - return TButton( - text: '底部弹出层-带标题及操作', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomConfirmPanel( - title: '标题文字标题文字标题文字标题文字标题文字标题文字标题文字标题文字', - leftText: '点这里确认!', - leftTextColor: TTheme.of(context).brandNormalColor, - leftClick: () { - TToast.showText('确认', context: context); - Navigator.maybePop(context); - }, - rightText: '关闭', - rightTextColor: TTheme.of(context).errorNormalColor, - rightClick: () => Navigator.maybePop(context), - child: Container(height: 200), - ); - }, - ), - ); - }, - ); - }), - ExampleItem( - desc: '带关闭超长文本', - builder: (_) { - return TButton( - text: '底部弹出层-带标题及操作', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomDisplayPanel( - title: '标题文字标题文字标题文字标题文字标题文字标题文字标题文字', - closeColor: TTheme.of(context).errorNormalColor, - closeClick: () => Navigator.maybePop(context), - child: Container(height: 200), - ); - }), - ); - }, - ); - }), - ExampleItem( - desc: '修改圆角', - builder: (_) { - return Column( - // spacing: 16, - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - TButton( - text: '底部弹出层-修改圆角', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomDisplayPanel( - title: '标题文字标题文字标题文字标题文字标题文字标题文字标题文字', - closeColor: - TTheme.of(context).errorNormalColor, - closeClick: () => Navigator.maybePop(context), - child: Container(height: 200), - radius: 6, - ); - }), - ); - }, - ), - const SizedBox(height: 16), - TButton( - text: '底部弹出层-修改圆角', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomConfirmPanel( - title: '标题文字标题文字标题文字标题文字标题文字标题文字标题文字', - leftText: '点这里确认!', - leftTextColor: - TTheme.of(context).brandNormalColor, - leftClick: () { - TToast.showText('确认', context: context); - Navigator.maybePop(context); - }, - rightText: '关闭', - rightTextColor: - TTheme.of(context).errorNormalColor, - rightClick: () => Navigator.maybePop(context), - child: Container(height: 200), - radius: 6, - ); - }), - ); - }, - ), - const SizedBox(height: 16), - TButton( - text: '居中弹出层-修改圆角', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.center, - builder: (context) { - return TPopupCenterPanel( - closeColor: - TTheme.of(context).errorNormalColor, - closeClick: () { - Navigator.maybePop(context); - }, - child: const SizedBox(height: 240, width: 240), - radius: 6, - ); - }), - ); - }, - ), - const SizedBox(height: 16), - TButton( - text: '居中弹出层-底部关闭-修改圆角', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.center, - builder: (context) { - return TPopupCenterPanel( - closeUnderBottom: true, - closeClick: () { - Navigator.maybePop(context); - }, - child: const SizedBox(height: 240, width: 240), - radius: 6, - ); - }), - ); - }, - ), - ], - ); - }), + desc: '操作栏超长文本,指定颜色', + builder: (_) { + return TButton( + text: '底部弹出层-带标题及操作', + isBlock: true, + theme: TButtonTheme.primary, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () { + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 280, + titleWidget: TText('标题文字标题文字标题文字标题文字标题文字标题文字标题文字'), + cancelBuilder: (_, close) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: close, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: TText( + '点这里确认!', + textColor: TTheme.of(context).brandNormalColor, + font: TTheme.of(context).fontBodyLarge, + ), + ), + ), + confirmBuilder: (_, close) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: close, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: TText( + '关闭', + textColor: TTheme.of(context).errorNormalColor, + font: TTheme.of(context).fontBodyLarge, + ), + ), + ), + child: Container(height: 200)), + ); + }, + ); + }, + ), ExampleItem( - desc: '自定义位置', + desc: '带关闭超长文本', builder: (_) { return TButton( - text: '自定义位置', + text: '底部弹出层-带标题及关闭', isBlock: true, theme: TButtonTheme.primary, type: TButtonType.outline, size: TButtonSize.large, onTap: () { - var renderBox = - navBarkey.currentContext!.findRenderObject() as RenderBox; - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.right, - modalTop: renderBox.size.height, - builder: (context) { - return Container( - color: TTheme.of(context).bgColorContainer, - width: 280, - ); - }, - ), + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 280, + headerBuilder: _bottomTitleCloseHeader( + title: '标题文字标题文字标题文字标题文字标题文字标题文字标题文字', + ), + child: Container(height: 200)), ); }, ); }, ), ExampleItem( - desc: '弹出层包含输入框且不会被键盘遮挡', - builder: (_) { - return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceAround, - // spacing: 16, - children: [ - TButton( - text: '底部弹出层-键盘弹出默认遮挡', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomDisplayPanel( - title: '标题文字标题文字标题文字标题文字标题文字标题文字标题文字', - closeColor: - TTheme.of(context).errorNormalColor, - closeClick: () => Navigator.maybePop(context), - child: Material( - child: SizedBox( - height: 100, - child: TInput( - type: TInputType.normal, - leftLabel: '标签文字', - hintText: '请输入文字', - maxLength: 10, - additionInfo: '最大输入10个字符', - ), + desc: '修改圆角', + builder: (_) { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + TButton( + text: '底部弹出层-修改圆角', + isBlock: true, + theme: TButtonTheme.primary, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () { + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 280, + radius: 6, + headerBuilder: _bottomTitleCloseHeader( + title: '标题文字标题文字标题文字标题文字标题文字标题文字标题文字', + ), + child: Container(height: 200)), + ); + }, + ), + const SizedBox(height: 16), + TButton( + text: '底部弹出层-修改圆角', + isBlock: true, + theme: TButtonTheme.primary, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () { + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 280, + radius: 6, + titleWidget: TText('标题文字标题文字标题文字标题文字标题文字标题文字标题文字'), + cancelBuilder: (_, close) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: close, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 8), + child: TText( + '点这里确认!', + textColor: TTheme.of(context).brandNormalColor, + font: TTheme.of(context).fontBodyLarge, ), ), - radius: 6, - ); - }), - ); - }, - ), - const SizedBox(height: 16), - TButton( - text: '底部弹出层-键盘弹出不遮挡', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - focusMove: true, - builder: (context) { - return TPopupBottomDisplayPanel( - title: '标题文字标题文字标题文字标题文字标题文字标题文字标题文字', - closeColor: - TTheme.of(context).errorNormalColor, - closeClick: () { - Navigator.maybePop(context); - }, - child: Material( - child: SizedBox( - height: 100, - child: TInput( - type: TInputType.normal, - leftLabel: '标签文字', - hintText: '请输入文字', - maxLength: 10, - additionInfo: '最大输入10个字符', - ), + ), + confirmBuilder: (_, close) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: close, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 8), + child: TText( + '关闭', + textColor: TTheme.of(context).errorNormalColor, + font: TTheme.of(context).fontBodyLarge, ), ), - radius: 6, - ); - }), - ); - }, - ), - const SizedBox(height: 16), - TButton( - text: '居中弹出层-键盘弹出不遮挡', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.center, - focusMove: true, - builder: (context) { - return TPopupCenterPanel( - closeColor: - TTheme.of(context).errorNormalColor, - closeClick: () { - Navigator.maybePop(context); - }, - child: SizedBox( - height: 348, - child: Column( - children: [ - TInput( - type: TInputType.normal, - leftLabel: '标签文字1', - hintText: '请输入文字1', - maxLength: 10, - ), - TInput( - type: TInputType.normal, - leftLabel: '标签文字2', - hintText: '请输入文字2', - maxLength: 10, - ), - TInput( - type: TInputType.normal, - leftLabel: '标签文字3', - hintText: '请输入文字3', - maxLength: 10, - ), - TInput( - type: TInputType.normal, - leftLabel: '标签文字4', - hintText: '请输入文字4', - maxLength: 10, - ), - TInput( - type: TInputType.normal, - leftLabel: '会被键盘遮挡的输入框1', - hintText: '会被键盘遮挡小部分', - maxLength: 10, - ), - TInput( - type: TInputType.normal, - leftLabel: '会被键盘遮挡的输入框2', - hintText: '会被键盘遮挡全遮挡', - maxLength: 10, - ) - ], - ), + ), + child: Container(height: 200)), + ); + }, + ), + const SizedBox(height: 16), + TButton( + text: '居中弹出层-修改圆角', + isBlock: true, + theme: TButtonTheme.primary, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () { + TPopup.show( + context, + options: TPopupOptions.center( + width: 240, + height: 240, + radius: 6, + closeBuilder: (_, close) => IconButton( + icon: Icon( + TIcons.close_circle, + color: TTheme.of(context).errorNormalColor, + size: 32, ), - radius: 6, - ); - }), - ); - }, - ) - ], - ); - }), + onPressed: close, + ), + child: const SizedBox(height: 240, width: 240)), + ); + }, + ), + const SizedBox(height: 16), + TButton( + text: '居中弹出层-底部关闭-修改圆角', + isBlock: true, + theme: TButtonTheme.primary, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () { + TPopup.show( + context, + options: TPopupOptions.center( + width: 240, + height: 240, + radius: 6, + child: const SizedBox(height: 240, width: 240)), + ); + }, + ), + ], + ); + }, + ), ExampleItem( - /// todo fix 动画闪烁 - desc: '可拖动全屏', + desc: '自定义位置', builder: (_) { - return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceAround, - // spacing: 16, - children: [ - TButton( - text: '可拖动全屏', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomDisplayPanel( - title: '标题文字', - draggable: true, - closeColor: - TTheme.of(context).errorNormalColor, - closeClick: () { - Navigator.maybePop(context); - }, - child: Container(height: 200), - ); - }), - ); - }, - ), - const SizedBox(height: 16), - TButton( - text: '可拖动全屏-带标题及操作', - isBlock: true, - theme: TButtonTheme.primary, - type: TButtonType.outline, - size: TButtonSize.large, - onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomConfirmPanel( - title: '标题文字', - draggable: true, - leftClick: () { - Navigator.maybePop(context); - }, - rightClick: () { - TToast.showText('确定', context: context); - Navigator.maybePop(context); - }, - child: Container(height: 200), - ); - }), - ); - }, - ), - ]); + return TButton( + text: '自定义位置', + isBlock: true, + theme: TButtonTheme.primary, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () { + final renderBox = + navBarkey.currentContext!.findRenderObject() as RenderBox; + TPopup.show( + context, + options: TPopupOptions.right( + width: 280, + inset: TPopupRightInset(top: renderBox.size.height), + child: Container( + color: TTheme.of(context).bgColorContainer, + )), + ); + }, + ); }, ), ], ); } + // --- 01 组件类型(保持原 Demo 文案与交互)--- + @Demo(group: 'popup') Widget _buildPopFromTop(BuildContext context) { return TButton( @@ -473,21 +316,16 @@ class TPopupPage extends StatelessWidget { type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.top, - open: () { - print('open'); - }, - opened: () { - print('opened'); - }, - builder: (context) { - return Container( - color: TTheme.of(context).bgColorContainer, - height: 240, - ); - }), + TPopup.show( + context, + options: TPopupOptions.top( + height: 240, + onOpen: () => print('open'), + onOpened: () => print('opened'), + child: Container( + color: TTheme.of(context).bgColorContainer, + height: 240, + )), ); }, ); @@ -502,15 +340,13 @@ class TPopupPage extends StatelessWidget { type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.left, - builder: (context) { - return Container( - color: TTheme.of(context).bgColorContainer, - width: 280, - ); - }), + TPopup.show( + context, + options: TPopupOptions.left( + width: 280, + child: Container( + color: TTheme.of(context).bgColorContainer, + )), ); }, ); @@ -525,20 +361,19 @@ class TPopupPage extends StatelessWidget { type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.center, - builder: (context) { - return Container( - decoration: BoxDecoration( - color: TTheme.of(context).bgColorContainer, - borderRadius: - BorderRadius.circular(TTheme.of(context).radiusLarge), - ), - width: 240, - height: 240, - ); - }), + TPopup.show( + context, + options: TPopupOptions.center( + closeBuilder: null, + child: Container( + decoration: BoxDecoration( + color: TTheme.of(context).bgColorContainer, + borderRadius: + BorderRadius.circular(TTheme.of(context).radiusLarge), + ), + width: 240, + height: 240, + )), ); }, ); @@ -553,15 +388,15 @@ class TPopupPage extends StatelessWidget { type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return Container( - color: TTheme.of(context).bgColorContainer, - height: 240, - ); - }), + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 240, + headerBuilder: null, + child: Container( + color: TTheme.of(context).bgColorContainer, + height: 240, + )), ); }, ); @@ -576,77 +411,102 @@ class TPopupPage extends StatelessWidget { type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.right, - builder: (context) { - return Container( - color: TTheme.of(context).bgColorContainer, - width: 280, - ); - }), + TPopup.show( + context, + options: TPopupOptions.right( + width: 280, + child: Container( + color: TTheme.of(context).bgColorContainer, + )), ); }, ); } + // --- 02 组件示例 --- + + /// 外层 Popup 的 child 内再 `TPopup.show(innerContext, options: …)`:用各自 [TPopupHandle] 关闭。 @Demo(group: 'popup') - Widget _buildPopFromBottomWithOperationAndTitle(BuildContext context) { + Widget _buildNestedPopup(BuildContext context) { return TButton( - text: '底部弹出层-带标题及操作', + text: '内层再弹一层(嵌套叠加)', isBlock: true, theme: TButtonTheme.primary, type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomConfirmPanel( - title: '标题文字', - leftClick: () { - Navigator.maybePop(context); - }, - rightClick: () { - TToast.showText('确定', context: context); - Navigator.maybePop(context); + TPopupHandle? outerHandle; + outerHandle = TPopup.show( + context, + options: TPopupOptions.bottom( + height: 360, + headerBuilder: null, + child: Builder( + builder: (innerContext) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TText( + '外层:headerBuilder: null,仅 child', + textColor: TTheme.of(innerContext).textColorSecondary, + ), + const SizedBox(height: 16), + TButton( + text: '打开内层 Popup', + isBlock: true, + theme: TButtonTheme.primary, + size: TButtonSize.large, + onTap: () { + TPopup.show( + innerContext, + options: TPopupOptions.bottom( + height: 280, + titleWidget: const TText('内层标题'), + child: Container( + height: 160, + color: TTheme.of(innerContext) + .bgColorSecondaryContainer, + ), + ), + ); + }, + ), + const SizedBox(height: 12), + TButton( + text: '关闭外层', + isBlock: true, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () => outerHandle?.close(), + ), + ], + ), + ); }, - child: Container(height: 200), - ); - }, - ), + )), ); }, ); } @Demo(group: 'popup') - Widget _buildPopFromBottomWithOperation(BuildContext context) { + Widget _buildPopFromBottomWithOperationAndTitle(BuildContext context) { return TButton( - text: '底部弹出层-带操作', + text: '底部弹出层-带标题及操作', isBlock: true, theme: TButtonTheme.primary, type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push(TSlidePopupRoute( - modalBarrierColor: TTheme.of(context).fontGyColor2, - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomConfirmPanel( - leftClick: () { - Navigator.maybePop(context); - }, - rightClick: () { - TToast.showText('确定', context: context); - Navigator.maybePop(context); - }, - child: Container( - height: 200, - ), - ); - })); + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 280, + titleWidget: TText('标题文字'), + child: Container(height: 200)), + ); }, ); } @@ -660,150 +520,209 @@ class TPopupPage extends StatelessWidget { type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomDisplayPanel( - title: '标题文字', - closeClick: () { - Navigator.maybePop(context); - }, - child: Container(height: 200), - ); - }), + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 280, + cancelBuilder: (_, close) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: close, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: TText( + '关闭', + textColor: TTheme.of(context).textColorSecondary, + font: TTheme.of(context).fontBodyLarge, + ), + ), + ), + titleWidget: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(TIcons.info_circle, + color: TTheme.of(context).brandNormalColor, size: 18), + const SizedBox(width: 4), + TText( + '自定义标题', + textColor: TTheme.of(context).brandNormalColor, + font: TTheme.of(context).fontTitleMedium, + ), + ], + ), + confirmBuilder: (_, close) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: close, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: TText( + '完成', + textColor: TTheme.of(context).brandNormalColor, + font: TTheme.of(context).fontTitleMedium, + fontWeight: FontWeight.w600, + ), + ), + ), + child: Container(height: 200)), ); }, ); } @Demo(group: 'popup') - Widget _buildPopFromBottomWithCloseAndLeftTitle(BuildContext context) { + Widget _buildPopFromCenterWithClose(BuildContext context) { return TButton( - text: '底部弹出层-带左边标题及关闭', + text: '居中弹出层-带关闭', isBlock: true, theme: TButtonTheme.primary, type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomDisplayPanel( - title: '标题文字', - titleLeft: true, - closeClick: () { - Navigator.maybePop(context); - }, - child: Container(height: 200), - ); - }), + TPopup.show( + context, + options: TPopupOptions.center( + closeOnOverlayClick: false, + width: 240, + height: 240, + closeBuilder: (_, close) => IconButton( + icon: Icon( + TIcons.close_circle, + color: TTheme.of(context).fontWhColor1, + size: 32, + ), + onPressed: close, + ), + child: const SizedBox(width: 240, height: 240)), ); }, ); } @Demo(group: 'popup') - Widget _buildPopFromBottomWithClose(BuildContext context) { + Widget _buildPopFromCenterWithUnderClose(BuildContext context) { return TButton( - text: '底部弹出层-带关闭', + text: '居中弹出层-自定义下方按钮', isBlock: true, theme: TButtonTheme.primary, type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomDisplayPanel( - closeClick: () { - Navigator.maybePop(context); - }, - child: Container(height: 200), - ); - }), + TPopup.show( + context, + options: TPopupOptions.center( + closeOnOverlayClick: true, + width: 240, + height: 200, + closeBuilder: (_, close) => IconButton( + icon: Icon( + TIcons.poweroff, + color: TTheme.of(context).fontWhColor1, + size: 36, + ), + onPressed: close, + ), + child: Container( + width: 240, + height: 200, + color: TTheme.of(context).bgColorContainer, + )), ); }, ); } + // --- 更多 API --- + @Demo(group: 'popup') - Widget _buildPopFromBottomWithTitle(BuildContext context) { + Widget _buildApiInset(BuildContext context) { return TButton( - text: '底部弹出层-仅标题', + text: 'bottom inset.left/right', isBlock: true, theme: TButtonTheme.primary, type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (context) { - return TPopupBottomDisplayPanel( - title: '标题文字', - hideClose: true, - // closeClick: () { - // Navigator.maybePop(context); - // }, - child: Container(height: 200), - ); - }), + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 320, + inset: const TPopupBottomInset(left: 16, right: 16), + titleWidget: TText('左右留白'), + child: Container( + height: 240, + color: TTheme.of(context).bgColorContainer, + )), ); }, ); } @Demo(group: 'popup') - Widget _buildPopFromCenterWithClose(BuildContext context) { + Widget _buildApiShowOverlayFalse(BuildContext context) { return TButton( - text: '居中弹出层-带关闭', + text: 'showOverlay: false(透明模态)', isBlock: true, theme: TButtonTheme.primary, type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - isDismissible: false, - slideTransitionFrom: SlideTransitionFrom.center, - builder: (context) { - return TPopupCenterPanel( - closeClick: () { - Navigator.maybePop(context); - }, - child: const SizedBox(width: 240, height: 240), - ); - }), + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 280, + showOverlay: false, + modal: true, + // 不显示可见蒙层,但仍阻断背景交互;须保留其它关闭入口。 + titleWidget: const TText('透明模态'), + child: Container( + height: 200, + color: TTheme.of(context).bgColorContainer, + )), ); }, ); } @Demo(group: 'popup') - Widget _buildPopFromCenterWithUnderClose(BuildContext context) { + Widget _buildApiOnOverlayClick(BuildContext context) { return TButton( - text: '居中弹出层-关闭在下方', + text: 'onOverlayClick', isBlock: true, theme: TButtonTheme.primary, type: TButtonType.outline, size: TButtonSize.large, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - isDismissible: false, - slideTransitionFrom: SlideTransitionFrom.center, - builder: (context) { - return TPopupCenterPanel( - closeUnderBottom: true, - closeClick: () { - Navigator.maybePop(context); - }, - child: const SizedBox(width: 240, height: 240), - ); - }), + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 260, + onOverlayClick: () => TToast.showText('点击蒙层', context: context), + child: Container( + height: 200, + color: TTheme.of(context).bgColorContainer, + )), + ); + }, + ); + } + + @Demo(group: 'popup') + Widget _buildApiDuration(BuildContext context) { + return TButton( + text: 'animationDuration: 600ms', + isBlock: true, + theme: TButtonTheme.primary, + type: TButtonType.outline, + size: TButtonSize.large, + onTap: () { + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 240, + animationDuration: const Duration(milliseconds: 600), + child: Container( + height: 200, + color: TTheme.of(context).bgColorContainer, + )), ); }, ); diff --git a/tdesign-component/lib/src/components/action_sheet/t_action_sheet.dart b/tdesign-component/lib/src/components/action_sheet/t_action_sheet.dart index 9117f6a3d..8e51a6902 100644 --- a/tdesign-component/lib/src/components/action_sheet/t_action_sheet.dart +++ b/tdesign-component/lib/src/components/action_sheet/t_action_sheet.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../../util/context_extension.dart'; -import '../popup/t_popup_route.dart'; +import '../popup/t_popup.dart'; import 't_action_sheet.dart'; import 't_action_sheet_grid.dart'; import 't_action_sheet_group.dart'; @@ -9,7 +9,8 @@ import 't_action_sheet_list.dart'; export 't_action_sheet_item.dart'; -typedef TActionSheetItemCallback = void Function(TActionSheetItem item, int index); +typedef TActionSheetItemCallback = void Function( + TActionSheetItem item, int index); enum TActionSheetTheme { list, grid, group } @@ -112,7 +113,7 @@ class TActionSheet { /// 使用安全区域 final bool useSafeArea; - static TSlidePopupRoute? _actionSheetRoute; + static TPopupHandle? _actionSheetHandle; /// 显示列表类型面板 static void showListActionSheet( @@ -253,9 +254,7 @@ class TActionSheet { @mustCallSuper void close() { - if (_actionSheetRoute != null) { - Navigator.of(context).pop(); - } + _actionSheetHandle?.close(); } /// 创建路由 @@ -280,66 +279,70 @@ class TActionSheet { VoidCallback? onClose, bool useSafeArea = true, }) { - if (_actionSheetRoute != null) { + if (_actionSheetHandle?.isShowing == true) { return; } cancelText = cancelText ?? context.resource.cancel; - _actionSheetRoute = TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.bottom, - isDismissible: showOverlay ? closeOnOverlayClick : false, - modalBarrierColor: showOverlay ? null : Colors.transparent, - builder: (context) { - switch (theme) { - case TActionSheetTheme.list: - return TActionSheetList( - items: items, - align: align, - cancelText: cancelText, - description: description, - showCancel: showCancel, - onCancel: onCancel, - onSelected: onSelected, - useSafeArea: useSafeArea, - ); - case TActionSheetTheme.grid: - return TActionSheetGrid( - items: items, - align: align, - onSelected: onSelected, - showCancel: showCancel, - showPagination: showPagination, - scrollable: scrollable, - cancelText: cancelText, - description: description, - count: count, - rows: rows, - onCancel: onCancel, - itemHeight: itemHeight, - itemMinWidth: itemMinWidth, - useSafeArea: useSafeArea, - ); - case TActionSheetTheme.group: - return TActionSheetGroup( - items: items, - align: align, - cancelText: cancelText, - showCancel: showCancel, - onCancel: onCancel, - onSelected: onSelected, - itemHeight: itemHeight, - itemMinWidth: itemMinWidth, - useSafeArea: useSafeArea, - ); - default: - return const SizedBox.shrink(); - } - }, + Widget sheetChild; + switch (theme) { + case TActionSheetTheme.list: + sheetChild = TActionSheetList( + items: items, + align: align, + cancelText: cancelText, + description: description, + showCancel: showCancel, + onCancel: onCancel, + onSelected: onSelected, + useSafeArea: useSafeArea, + ); + break; + case TActionSheetTheme.grid: + sheetChild = TActionSheetGrid( + items: items, + align: align, + onSelected: onSelected, + showCancel: showCancel, + showPagination: showPagination, + scrollable: scrollable, + cancelText: cancelText, + description: description, + count: count, + rows: rows, + onCancel: onCancel, + itemHeight: itemHeight, + itemMinWidth: itemMinWidth, + useSafeArea: useSafeArea, + ); + break; + case TActionSheetTheme.group: + sheetChild = TActionSheetGroup( + items: items, + align: align, + cancelText: cancelText, + showCancel: showCancel, + onCancel: onCancel, + onSelected: onSelected, + itemHeight: itemHeight, + itemMinWidth: itemMinWidth, + useSafeArea: useSafeArea, + ); + break; + } + + _actionSheetHandle = TPopup.show( + context, + options: TPopupOptions.bottom( + cancelBuilder: null, + confirmBuilder: null, + showOverlay: showOverlay, + closeOnOverlayClick: showOverlay && closeOnOverlayClick, + overlayColor: showOverlay ? null : Colors.transparent, + onClosed: onClose, + child: sheetChild, + ), ); - Navigator.of(context).push(_actionSheetRoute!).then((_) { - _actionSheetRoute = null; - onClose?.call(); - }); } } diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart index ad280a989..2898b14bc 100644 --- a/tdesign-component/lib/src/components/calendar/t_calendar.dart +++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../../tdesign_flutter.dart'; @@ -60,7 +62,7 @@ class TCalendarInherited extends InheritedWidget { /// 是否由 [TCalendar] 自行渲染关闭按钮和标题行。 /// /// 为 `true`(默认)时 [TCalendar] 渲染关闭按钮与标题行; - /// 为 `false` 时由外层面板(如 [TPopupBottomDisplayPanel])承载。 + /// 为 `false` 时由外层弹窗容器承载。 final bool popupControls; /// 是否由 [TCalendar] 渲染底部确认按钮。 @@ -275,8 +277,8 @@ class TCalendar extends StatefulWidget { /// } /// ``` /// - /// 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] - /// + [TSlidePopupRoute] 自行组装。 + /// 若需完全自定义布局,请直接使用 [TCalendar] + [TPopup.show] + /// / [TPopupOptions.bottom] 自行组装。 static Future?> showPopup( BuildContext context, { /// 弹窗标题组件 @@ -352,64 +354,88 @@ class TCalendar extends StatefulWidget { Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, }) async { final selected = ValueNotifier>(initialValue ?? []); + final completer = Completer?>(); + TPopupHandle? handle; List? result; - var closing = false; + var closed = false; - void doClose(NavigatorState nav) { - if (closing) { + void closePopup() { + if (closed) { return; } - closing = true; - nav.pop(); + handle?.close(); } - await Navigator.of(context).push(TSlidePopupRoute( - isDismissible: autoClose, - slideTransitionFrom: SlideTransitionFrom.bottom, - builder: (ctx) { - final nav = Navigator.of(context); - final safeBottom = MediaQuery.of(ctx).padding.bottom; - final screenHeight = MediaQuery.of(ctx).size.height; - - final panelHeight = - popupHeight ?? _calcDefaultHeight(safeBottom, screenHeight); - - // 提取标题文字给 TPopupBottomDisplayPanel - String? panelTitle; - if (titleWidget != null) { - panelTitle = _extractTextFromWidget(titleWidget); - } + void completeClose() { + if (closed) { + return; + } + closed = true; + onClose?.call(); + if (!completer.isCompleted) { + completer.complete(result); + } + } - return TCalendarInherited( - selected: selected, - usePopup: true, - popupControls: false, - popupConfirmBtn: true, - confirmBtnBuilder: confirmBtnBuilder, - popupBottomBuilder: popupBottomBuilder, - popupBottomExpanded: popupBottomExpanded, - onClose: () { - if (autoClose) { - doClose(nav); - } - }, - onConfirm: () { - result = List.from(selected.value); - onConfirm?.call(result!); - if (autoClose) { - doClose(nav); - } - }, - child: TPopupBottomDisplayPanel( - title: panelTitle, - draggable: draggable, - fixedHeight: panelHeight, - closeClick: () { + final mediaQuery = MediaQuery.of(context); + final panelHeight = popupHeight ?? + _calcDefaultHeight(mediaQuery.padding.bottom, mediaQuery.size.height); + final calendarHeight = + (panelHeight - _kPanelHeaderHeight).clamp(0.0, double.infinity); + + handle = TPopup.show( + context, + options: TPopupOptions.bottom( + headerBuilder: (ctx, close) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Stack( + alignment: Alignment.center, + children: [ + if (titleWidget != null) Center(child: titleWidget), + if (autoClose) + Positioned( + right: -8, + child: IconButton( + icon: Icon( + TIcons.close, + color: style?.titleCloseColor, + ), + onPressed: close, + ), + ), + ], + ), + ), + titleWidget: null, + cancelBuilder: null, + confirmBuilder: null, + height: panelHeight, + closeOnOverlayClick: autoClose, + onClosed: completeClose, + child: PopScope( + canPop: autoClose, + child: TCalendarInherited( + selected: selected, + usePopup: true, + popupControls: false, + popupConfirmBtn: true, + confirmBtnBuilder: confirmBtnBuilder, + popupBottomBuilder: popupBottomBuilder, + popupBottomExpanded: popupBottomExpanded, + onClose: () { + if (autoClose) { + closePopup(); + } + }, + onConfirm: () { + result = List.from(selected.value); + onConfirm?.call(result!); if (autoClose) { - doClose(nav); + closePopup(); } }, child: TCalendar( + height: calendarHeight, titleWidget: titleWidget, type: type, initialValue: initialValue, @@ -428,27 +454,11 @@ class TCalendar extends StatefulWidget { monthTitleBuilder: monthTitleBuilder, ), ), - ); - }, - )); - - onClose?.call(); - return result; - } + ), + ), + ); - /// 尝试从 Widget 中提取文本内容,用于 TPopupBottomDisplayPanel 的标题。 - /// 仅支持 Text 和 RichText 两种常见情况。 - static String? _extractTextFromWidget(Widget widget) { - if (widget is Text) { - return widget.data; - } - if (widget is RichText) { - final text = widget.text; - if (text is TextSpan) { - return text.toPlainText(); - } - } - return null; + return completer.future; } @override diff --git a/tdesign-component/lib/src/components/drawer/t_drawer.dart b/tdesign-component/lib/src/components/drawer/t_drawer.dart index f4a4fea24..ac322e8f0 100644 --- a/tdesign-component/lib/src/components/drawer/t_drawer.dart +++ b/tdesign-component/lib/src/components/drawer/t_drawer.dart @@ -7,7 +7,7 @@ import '../cell/t_cell.dart'; import '../cell/t_cell_group.dart'; import '../cell/t_cell_style.dart'; import '../icon/t_icons.dart'; -import '../popup/t_popup_route.dart'; +import '../popup/t_popup.dart'; import 't_drawer_widget.dart'; /// 抽屉方向 @@ -98,25 +98,33 @@ class TDrawer { /// 是否显示最后一行分割线 final bool? isShowLastBordered; - TSlidePopupRoute? _drawerRoute; + TPopupHandle? _drawerHandle; void show() { - if (_drawerRoute != null) { - return; // 如果抽屉已经显示了,就不要再显示 + if (_drawerHandle?.isShowing == true) { + return; } final overlayEnabled = showOverlay ?? true; final dismissible = overlayEnabled && (closeOnOverlayClick ?? true); - - _drawerRoute = TSlidePopupRoute( - slideTransitionFrom: placement == TDrawerPlacement.right - ? SlideTransitionFrom.right - : SlideTransitionFrom.left, - isDismissible: dismissible, - modalBarrierColor: overlayEnabled ? null : Colors.transparent, - modalTop: drawerTop, - builder: (context) { - return TDrawerWidget( + final popupPlacement = placement == TDrawerPlacement.right + ? TPopupPlacement.right + : TPopupPlacement.left; + final popupInset = placement == TDrawerPlacement.right + ? TPopupRightInset(top: drawerTop ?? 0) + : TPopupLeftInset(top: drawerTop ?? 0); + + _drawerHandle = TPopup.show( + context, + options: TPopupOptions( + placement: popupPlacement, + width: width, + inset: popupInset, + showOverlay: overlayEnabled, + closeOnOverlayClick: dismissible, + overlayColor: overlayEnabled ? null : Colors.transparent, + onClosed: _deleteRouter, + child: TDrawerWidget( footer: footer, items: items, contentWidget: contentWidget, @@ -129,14 +137,9 @@ class TDrawer { backgroundColor: backgroundColor, bordered: bordered, isShowLastBordered: isShowLastBordered, - ); - }, + ), + ), ); - - Navigator.of(context).push(_drawerRoute!).then((_) { - // 当抽屉关闭时,将_drawerRoute置为null - _deleteRouter(); - }); } void open() { @@ -145,14 +148,11 @@ class TDrawer { @mustCallSuper void close() { - if (_drawerRoute != null) { - Navigator.of(context).pop(); - _deleteRouter(); - } + _drawerHandle?.close(); } void _deleteRouter() { - _drawerRoute = null; + _drawerHandle = null; onClose?.call(); } } diff --git a/tdesign-component/lib/src/components/image_viewer/t_image_viewer.dart b/tdesign-component/lib/src/components/image_viewer/t_image_viewer.dart index afb7b4906..d22b14063 100644 --- a/tdesign-component/lib/src/components/image_viewer/t_image_viewer.dart +++ b/tdesign-component/lib/src/components/image_viewer/t_image_viewer.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import '../../theme/t_colors.dart'; import '../../theme/t_theme.dart'; -import '../popup/t_popup_route.dart'; import 't_image_viewer_widget.dart'; /// 图片预览工具 diff --git a/tdesign-component/lib/src/components/picker/t_picker.dart b/tdesign-component/lib/src/components/picker/t_picker.dart index ced374659..762891e5c 100644 --- a/tdesign-component/lib/src/components/picker/t_picker.dart +++ b/tdesign-component/lib/src/components/picker/t_picker.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../../tdesign_flutter.dart'; import '../../util/context_extension.dart'; +import '../../util/t_toolbar_pressable.dart'; import 'no_wave_behavior.dart'; // =============== 文件级常量(魔法数字归一) =============== @@ -10,12 +11,6 @@ import 'no_wave_behavior.dart'; /// 工具栏高度(px) const double _kToolbarHeight = 48; -/// 按钮按压态动画时长 -const Duration _kPressAnimDuration = Duration(milliseconds: 100); - -/// 按钮按压态透明度 -const double _kPressedOpacity = 0.5; - /// disabled 项修正动画 - 距离 ≤ 2 时的时长 const int _kCorrectAnimShortMs = 200; @@ -245,8 +240,6 @@ class _TPickerState extends State { final _scrollBehavior = NoWaveBehavior(); /// 工具栏按钮按压态(参考 TCheckbox 的反馈方式) - bool _cancelPressed = false; - bool _confirmPressed = false; double get _itemHeight => widget.height / widget.itemCount; @@ -399,23 +392,31 @@ class _TPickerState extends State { padding: EdgeInsets.symmetric(horizontal: theme.spacer16), child: Row( children: [ - _buildToolbarButton( - theme: theme, - pressed: _cancelPressed, - onPressChange: (v) => setState(() => _cancelPressed = v), - onTap: () => widget.onCancel?.call(), - defaultColor: theme.fontGyColor2, + TToolbarPressable( + onTap: widget.onCancel, + mergeTextStyle: TextStyle( + fontSize: theme.fontTitleMedium?.size ?? _kDefaultFontSize, + color: theme.fontGyColor2, + ), + mergeIconTheme: IconThemeData( + color: theme.fontGyColor2, + size: _kDefaultIconSize, + ), child: cancelText, ), Expanded( child: Center(child: _buildTitle(theme)), ), - _buildToolbarButton( - theme: theme, - pressed: _confirmPressed, - onPressChange: (v) => setState(() => _confirmPressed = v), + TToolbarPressable( onTap: () => widget.onConfirm?.call(_buildValue()), - defaultColor: theme.brandNormalColor, + mergeTextStyle: TextStyle( + fontSize: theme.fontTitleMedium?.size ?? _kDefaultFontSize, + color: theme.brandNormalColor, + ), + mergeIconTheme: IconThemeData( + color: theme.brandNormalColor, + size: _kDefaultIconSize, + ), child: confirmText, ), ], @@ -441,48 +442,6 @@ class _TPickerState extends State { ); } - /// 带按压反馈的工具栏按钮 - /// - /// - 按下时 [AnimatedOpacity] 平滑过渡到 [_kPressedOpacity] - /// - [DefaultTextStyle] / [IconTheme] 为默认 [Text] / [Icon] 提供统一样式, - /// 用户传入的 Widget 若已指定样式,会优先采用自己的样式(merge 语义) - Widget _buildToolbarButton({ - required TThemeData theme, - required bool pressed, - required ValueChanged onPressChange, - required VoidCallback onTap, - required Color defaultColor, - required Widget child, - }) { - final styledChild = DefaultTextStyle.merge( - style: TextStyle( - fontSize: theme.fontTitleMedium?.size ?? _kDefaultFontSize, - color: defaultColor, - ), - child: IconTheme.merge( - data: IconThemeData(color: defaultColor, size: _kDefaultIconSize), - child: child, - ), - ); - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTapDown: (_) => onPressChange(true), - onTapUp: (_) => onPressChange(false), - onTapCancel: () => onPressChange(false), - onTap: onTap, - child: AnimatedOpacity( - duration: _kPressAnimDuration, - opacity: pressed ? _kPressedOpacity : 1.0, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: theme.spacer8, vertical: theme.spacer12), - child: styledChild, - ), - ), - ); - } - Widget _buildColumn(int colIndex) { final data = _columns[colIndex]; if (data.isEmpty) { diff --git a/tdesign-component/lib/src/components/popup/_popup_center_close.dart b/tdesign-component/lib/src/components/popup/_popup_center_close.dart new file mode 100644 index 000000000..778f1535e --- /dev/null +++ b/tdesign-component/lib/src/components/popup/_popup_center_close.dart @@ -0,0 +1,69 @@ +part of 't_popup.dart'; + +/// center 面板外下方关闭控件:默认图标与自定义关闭槽位都上报 [TPopupTrigger.close]。 +Widget buildPopupCenterCloseControl({ + required BuildContext context, + required TPopupOptions options, + required VoidCallback onCloseSlotTap, + required void Function(TPopupTrigger trigger) onCloseWithTrigger, +}) { + if (_isPopupDefaultClose(options.closeBuilder)) { + final theme = TTheme.of(context); + return IconButton( + tooltip: context.resource.close, + icon: Icon( + TIcons.close_circle, + color: theme.fontWhColor1, + size: 32, + ), + onPressed: () => onCloseWithTrigger(TPopupTrigger.close), + ); + } + return options.closeBuilder!(context, onCloseSlotTap); +} + +/// center 布局:内容面板 + 面板外下方关闭区。 +class PopupCenterUnderClose extends StatelessWidget { + const PopupCenterUnderClose({ + super.key, + required this.options, + required this.content, + required this.onCloseWithTrigger, + }); + + final TPopupOptions options; + final Widget content; + final void Function(TPopupTrigger trigger) onCloseWithTrigger; + + @override + Widget build(BuildContext context) { + var panel = content; + if (options.width != null || options.height != null) { + panel = SizedBox( + width: options.width, + height: options.height, + child: content, + ); + } + + void onCloseSlotTap() => onCloseWithTrigger(TPopupTrigger.close); + + final closeControl = buildPopupCenterCloseControl( + context: context, + options: options, + onCloseSlotTap: onCloseSlotTap, + onCloseWithTrigger: onCloseWithTrigger, + ); + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 40), + panel, + const SizedBox(height: 24), + closeControl, + ], + ); + } +} diff --git a/tdesign-component/lib/src/components/popup/_popup_header.dart b/tdesign-component/lib/src/components/popup/_popup_header.dart new file mode 100644 index 000000000..06ec0c0b0 --- /dev/null +++ b/tdesign-component/lib/src/components/popup/_popup_header.dart @@ -0,0 +1,177 @@ +part of 't_popup.dart'; + +/// bottom 头部渲染;行为由 [TPopupOptions.hasBuiltInHeader]、[TPopupOptions.useCustomHeader] 决定。 +class PopupHeader extends StatelessWidget { + const PopupHeader({ + super.key, + required this.options, + required this.onCloseWithTrigger, + }); + + final TPopupOptions options; + final void Function(TPopupTrigger trigger) onCloseWithTrigger; + + static const double headerHeight = 58; + + void _close() { + onCloseWithTrigger(TPopupTrigger.custom); + } + + @override + Widget build(BuildContext context) { + if (!options.hasBuiltInHeader) { + return const SizedBox.shrink(); + } + + if (options.useCustomHeader) { + return options.headerBuilder!(context, _close); + } + + return SizedBox( + height: headerHeight, + child: _DefaultHeader( + options: options, + onCloseWithTrigger: onCloseWithTrigger, + ), + ); + } +} + +class _DefaultHeader extends StatelessWidget { + const _DefaultHeader({ + required this.options, + required this.onCloseWithTrigger, + }); + + final TPopupOptions options; + + /// 给「内置 sentinel cancel/confirm 按钮」用的 close 入口, + /// 按钮自己传 [TPopupTrigger.cancel] / [TPopupTrigger.confirm]。 + final void Function(TPopupTrigger trigger) onCloseWithTrigger; + + @override + Widget build(BuildContext context) { + final theme = TTheme.of(context); + final showCancel = options.cancelBuilder != null; + final showConfirm = options.confirmBuilder != null; + + final title = options.titleWidget; + + return Row( + children: [ + if (showCancel) + Padding( + padding: EdgeInsets.only(left: theme.spacer8), + child: _wrapDefaultCancelSemantics( + context: context, + child: _buildCancel(context, theme), + ), + ) + else + SizedBox(width: theme.spacer16), + Expanded( + child: title == null + ? const SizedBox.shrink() + : Center(child: _titleWrap(context, theme, title)), + ), + if (showConfirm) + Padding( + padding: EdgeInsets.only(right: theme.spacer8), + child: _wrapDefaultConfirmSemantics( + context: context, + child: _buildConfirm(context, theme), + ), + ) + else + SizedBox(width: theme.spacer16), + ], + ); + } + + Widget _wrapDefaultCancelSemantics({ + required BuildContext context, + required Widget child, + }) { + if (!_isPopupDefaultCancel(options.cancelBuilder)) { + return child; + } + return Semantics( + button: true, + label: _cancelSemanticsLabel(context, options), + excludeSemantics: true, + child: child, + ); + } + + Widget _wrapDefaultConfirmSemantics({ + required BuildContext context, + required Widget child, + }) { + if (!_isPopupDefaultConfirm(options.confirmBuilder)) { + return child; + } + return Semantics( + button: true, + label: _confirmSemanticsLabel(context, options), + excludeSemantics: true, + child: child, + ); + } + + Widget _titleWrap(BuildContext context, TThemeData theme, Widget child) { + // 标题内容由用户插槽决定样式,这里只做布局约束。 + return DefaultTextStyle.merge( + style: TextStyle( + color: theme.textColorPrimary, + fontSize: theme.fontTitleLarge?.size, + fontWeight: FontWeight.w700, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: child, + ); + } + + Widget _buildCancel(BuildContext context, TThemeData theme) { + if (_isPopupDefaultCancel(options.cancelBuilder)) { + return TToolbarPressable( + onTap: () => onCloseWithTrigger(TPopupTrigger.cancel), + child: TText( + context.resource.cancel, + textColor: theme.textColorSecondary, + font: theme.fontBodyLarge, + ), + ); + } + return options.cancelBuilder!( + context, + () => onCloseWithTrigger(TPopupTrigger.cancel), + ); + } + + Widget _buildConfirm(BuildContext context, TThemeData theme) { + if (_isPopupDefaultConfirm(options.confirmBuilder)) { + return TToolbarPressable( + onTap: () => onCloseWithTrigger(TPopupTrigger.confirm), + child: TText( + context.resource.confirm, + textColor: theme.brandNormalColor, + font: theme.fontTitleMedium, + fontWeight: FontWeight.w600, + ), + ); + } + return options.confirmBuilder!( + context, + () => onCloseWithTrigger(TPopupTrigger.confirm), + ); + } +} + +String _cancelSemanticsLabel(BuildContext context, TPopupOptions options) { + return context.resource.cancel; +} + +String _confirmSemanticsLabel(BuildContext context, TPopupOptions options) { + return context.resource.confirm; +} diff --git a/tdesign-component/lib/src/components/popup/_popup_layout.dart b/tdesign-component/lib/src/components/popup/_popup_layout.dart new file mode 100644 index 000000000..d55207c02 --- /dev/null +++ b/tdesign-component/lib/src/components/popup/_popup_layout.dart @@ -0,0 +1,116 @@ +part of 't_popup.dart'; + +/// 按 [TPopupPlacement] 计算 [Positioned];center 仅居中,尺寸由 [PopupShell] 约束。 +class PopupLayout { + PopupLayout({ + required this.placement, + this.inset, + this.width, + this.height, + }); + + final TPopupPlacement placement; + final TPopupInset? inset; + final double? width; + final double? height; + + static const double defaultDrawerWidth = 280; + + Widget wrapPositioned({required Widget child}) { + switch (placement) { + case TPopupPlacement.top: + final inset = + this.inset is TPopupTopInset ? this.inset as TPopupTopInset : null; + return Positioned( + top: 0, + left: inset?.left ?? 0, + right: inset?.right ?? 0, + height: height, + child: child, + ); + case TPopupPlacement.bottom: + final inset = this.inset is TPopupBottomInset + ? this.inset as TPopupBottomInset + : null; + final bottomHeight = _bottomHeight(); + if (bottomHeight != null) { + return Positioned( + left: inset?.left ?? 0, + right: inset?.right ?? 0, + bottom: 0, + height: bottomHeight, + child: child, + ); + } + return Positioned( + left: inset?.left ?? 0, + right: inset?.right ?? 0, + bottom: 0, + child: child, + ); + case TPopupPlacement.left: + final inset = this.inset is TPopupLeftInset + ? this.inset as TPopupLeftInset + : null; + return Positioned( + top: inset?.top ?? 0, + bottom: inset?.bottom ?? 0, + left: 0, + width: width ?? defaultDrawerWidth, + child: child, + ); + case TPopupPlacement.right: + final inset = this.inset is TPopupRightInset + ? this.inset as TPopupRightInset + : null; + return Positioned( + top: inset?.top ?? 0, + bottom: inset?.bottom ?? 0, + right: 0, + width: width ?? defaultDrawerWidth, + child: child, + ); + case TPopupPlacement.center: + return Positioned.fill( + child: Center(child: child), + ); + } + } + + double? _bottomHeight() { + if (height != null) { + return height; + } + return null; + } + + Alignment get alignment { + switch (placement) { + case TPopupPlacement.top: + return Alignment.topCenter; + case TPopupPlacement.bottom: + return Alignment.bottomCenter; + case TPopupPlacement.left: + return Alignment.centerLeft; + case TPopupPlacement.right: + return Alignment.centerRight; + case TPopupPlacement.center: + return Alignment.center; + } + } + + Offset slideOffset(double t) { + switch (placement) { + case TPopupPlacement.top: + return Offset(0, t - 1); + case TPopupPlacement.bottom: + return Offset(0, 1 - t); + case TPopupPlacement.left: + return Offset(t - 1, 0); + case TPopupPlacement.right: + return Offset(1 - t, 0); + case TPopupPlacement.center: + return Offset.zero; + } + } +} diff --git a/tdesign-component/lib/src/components/popup/_popup_route.dart b/tdesign-component/lib/src/components/popup/_popup_route.dart new file mode 100644 index 000000000..1efb0b507 --- /dev/null +++ b/tdesign-component/lib/src/components/popup/_popup_route.dart @@ -0,0 +1,241 @@ +part of 't_popup.dart'; + +/// 库内 [PopupRoute];由 [TPopupHandle.open] push,勿在外部直接构造。 +class _PopupNavigatorRoute extends PopupRoute { + _PopupNavigatorRoute({ + required this.options, + required this.onCloseWithTrigger, + }) : _layout = PopupLayout( + placement: options.placement, + inset: options.inset, + width: options.width, + height: options.height, + ); + + final TPopupOptions options; + final void Function(TPopupTrigger trigger) onCloseWithTrigger; + + late PopupLayout _layout; + bool _animationListenerAttached = false; + bool _openedFired = false; + bool _closedFired = false; + bool _closeStartFired = false; + String? _barrierSemanticsLabel; + + _PopupBarrierMode get _barrierMode { + if (!options.modal) { + return _PopupBarrierMode.nonModal; + } + return options.showOverlay + ? _PopupBarrierMode.modalOverlay + : _PopupBarrierMode.modalTransparent; + } + + Color get _barrierColor { + if (!options.showOverlay) { + return Colors.transparent; + } + final base = options.overlayColor ?? Colors.black54; + if (options.overlayOpacity != null) { + final opacity = options.overlayOpacity!.clamp(0.0, 1.0); + return base.withValues(alpha: base.a * opacity); + } + return base; + } + + @override + Duration get transitionDuration => options.animationDuration; + + @override + Duration get reverseTransitionDuration => options.animationDuration; + + @override + bool get barrierDismissible => false; + + @override + String? get barrierLabel => + options.showOverlay ? _barrierSemanticsLabel : null; + + @override + Color? get barrierColor => null; + + /// 非 opaque,避免透明区域露出 Modal 默认底色。 + @override + bool get opaque => false; + + @override + bool get maintainState => !options.destroyOnClose; + + @override + Widget buildModalBarrier() { + if (_barrierMode == _PopupBarrierMode.nonModal) { + return const SizedBox.shrink(); + } + if (_barrierMode == _PopupBarrierMode.modalOverlay && + options.closeOnOverlayClick) { + return ModalBarrier( + color: Colors.transparent, + dismissible: true, + onDismiss: _handleOverlayTap, + semanticsLabel: _resolveBarrierSemanticsLabel(navigator!.context), + barrierSemanticsDismissible: true, + ); + } + return ModalBarrier( + color: Colors.transparent, + dismissible: false, + barrierSemanticsDismissible: false, + ); + } + + /// 关闭开始前统一入口:触发 [TPopupOptions.onClose]、[onVisibleChange](false, …)。 + void fireCloseStart(TPopupTrigger trigger) { + if (_closeStartFired) { + return; + } + _closeStartFired = true; + options.onVisibleChange?.call(false, trigger); + options.onClose?.call(); + } + + @override + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return const SizedBox.shrink(); + } + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + final curved = CurvedAnimation( + parent: animation, + curve: Curves.decelerate, + reverseCurve: Curves.easeOut, + ); + + _layout = PopupLayout( + placement: options.placement, + inset: options.inset, + width: options.width, + height: options.height, + ); + + final t = curved.value; + final shell = PopupShell( + options: options, + onCloseWithTrigger: onCloseWithTrigger, + ); + + Widget popupContent; + if (options.placement == TPopupPlacement.center) { + popupContent = Transform.scale( + scale: t, + alignment: Alignment.center, + child: shell, + ); + } else { + popupContent = FractionalTranslation( + translation: _layout.slideOffset(t), + child: shell, + ); + } + + final positioned = _layout.wrapPositioned(child: popupContent); + + final barrier = _buildBarrier(context, t); + + return Stack( + fit: StackFit.expand, + children: [ + if (_barrierMode == _PopupBarrierMode.modalOverlay) barrier, + positioned, + ], + ); + } + + Widget _buildBarrier(BuildContext context, double t) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _handleOverlayTap, + child: Container( + color: _barrierColor.withValues( + alpha: _barrierColor.a * t, + ), + ), + ); + } + + String _resolveBarrierSemanticsLabel(BuildContext context) { + return _barrierSemanticsLabel ??= + MaterialLocalizations.of(context).modalBarrierDismissLabel; + } + + void _handleOverlayTap() { + options.onOverlayClick?.call(); + if (options.closeOnOverlayClick) { + onCloseWithTrigger(TPopupTrigger.overlay); + } + } + + void _onAnimationStatus(AnimationStatus status) { + if (status == AnimationStatus.completed && !_openedFired) { + _openedFired = true; + options.onOpened?.call(); + } + if (status == AnimationStatus.dismissed && !_closedFired) { + _closedFired = true; + options.onClosed?.call(); + } + } + + void _attachAnimationListener() { + if (_animationListenerAttached) { + return; + } + final anim = animation; + if (anim == null) { + return; + } + _animationListenerAttached = true; + anim.addStatusListener(_onAnimationStatus); + if (anim.status == AnimationStatus.completed) { + _onAnimationStatus(AnimationStatus.completed); + } + } + + @override + TickerFuture didPush() { + options.onOpen?.call(); + options.onVisibleChange?.call(true, TPopupTrigger.api); + final future = super.didPush(); + future.whenComplete(_attachAnimationListener); + return future; + } + + @override + bool didPop(T? result) { + fireCloseStart(TPopupTrigger.systemBack); + return super.didPop(result); + } + + @override + void dispose() { + if (!_closedFired) { + _closedFired = true; + options.onClosed?.call(); + } + if (_animationListenerAttached) { + animation?.removeStatusListener(_onAnimationStatus); + } + super.dispose(); + } +} + +enum _PopupBarrierMode { modalOverlay, modalTransparent, nonModal } diff --git a/tdesign-component/lib/src/components/popup/_popup_shell.dart b/tdesign-component/lib/src/components/popup/_popup_shell.dart new file mode 100644 index 000000000..b4817d010 --- /dev/null +++ b/tdesign-component/lib/src/components/popup/_popup_shell.dart @@ -0,0 +1,106 @@ +part of 't_popup.dart'; + +/// 浮层内容外壳:圆角、[PopupHeader](仅 bottom)与主体内容; +/// center 由 [PopupCenterUnderClose] 接管面板外下方关闭区。 +class PopupShell extends StatelessWidget { + const PopupShell({ + super.key, + required this.options, + required this.onCloseWithTrigger, + }); + + final TPopupOptions options; + final void Function(TPopupTrigger trigger) onCloseWithTrigger; + + @override + Widget build(BuildContext context) { + final theme = TTheme.of(context); + final radius = options.radius ?? theme.radiusExtraLarge; + final backgroundColor = options.backgroundColor ?? theme.bgColorContainer; + final borderRadius = _borderRadius(options.placement, radius); + + if (options.placement == TPopupPlacement.center) { + return _buildCenter(context, radius, backgroundColor); + } + + return _buildEdge(context, borderRadius, backgroundColor); + } + + Widget _buildCenter(BuildContext context, double radius, Color background) { + final panel = Container( + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(radius), + ), + clipBehavior: Clip.antiAlias, + child: options.child, + ); + + if (options.closeBuilder != null) { + return PopupCenterUnderClose( + options: options, + content: panel, + onCloseWithTrigger: onCloseWithTrigger, + ); + } + return SizedBox( + width: options.width, + height: options.height, + child: panel, + ); + } + + Widget _buildEdge( + BuildContext context, + BorderRadius? borderRadius, + Color background, + ) { + final useExpanded = options.placement == TPopupPlacement.left || + options.placement == TPopupPlacement.right || + options.height != null; + + final body = options.placement == TPopupPlacement.bottom + ? Column( + mainAxisSize: useExpanded ? MainAxisSize.max : MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + PopupHeader( + options: options, + onCloseWithTrigger: onCloseWithTrigger, + ), + if (useExpanded) Expanded(child: options.child) else options.child, + ], + ) + : (useExpanded + ? Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [Expanded(child: options.child)], + ) + : options.child); + + return Container( + decoration: BoxDecoration( + color: background, + borderRadius: borderRadius, + ), + clipBehavior: Clip.antiAlias, + child: body, + ); + } + + BorderRadius? _borderRadius(TPopupPlacement placement, double radius) { + switch (placement) { + case TPopupPlacement.top: + return BorderRadius.vertical(bottom: Radius.circular(radius)); + case TPopupPlacement.bottom: + return BorderRadius.vertical(top: Radius.circular(radius)); + case TPopupPlacement.left: + return BorderRadius.horizontal(right: Radius.circular(radius)); + case TPopupPlacement.right: + return BorderRadius.horizontal(left: Radius.circular(radius)); + case TPopupPlacement.center: + return BorderRadius.circular(radius); + } + } +} diff --git a/tdesign-component/lib/src/components/popup/_popup_tracker.dart b/tdesign-component/lib/src/components/popup/_popup_tracker.dart new file mode 100644 index 000000000..2ed042d7c --- /dev/null +++ b/tdesign-component/lib/src/components/popup/_popup_tracker.dart @@ -0,0 +1,25 @@ +part of 't_popup.dart'; + +/// 库内:按 [Navigator] 记录 [TPopupHandle] 栈,用于嵌套与关闭清理。 +abstract class _PopupTracker { + static final Map> _stacks = {}; + + static void push(NavigatorState navigator, TPopupHandle handle) { + _stacks.putIfAbsent(navigator, () => []).add(handle); + } + + static void remove(NavigatorState navigator, TPopupHandle handle) { + _stacks[navigator]?.remove(handle); + if (_stacks[navigator]?.isEmpty ?? false) { + _stacks.remove(navigator); + } + } + + static TPopupHandle? top(NavigatorState navigator) { + final stack = _stacks[navigator]; + if (stack == null || stack.isEmpty) { + return null; + } + return stack.last; + } +} diff --git a/tdesign-component/lib/src/components/popup/t_popup.dart b/tdesign-component/lib/src/components/popup/t_popup.dart new file mode 100644 index 000000000..4d175352d --- /dev/null +++ b/tdesign-component/lib/src/components/popup/t_popup.dart @@ -0,0 +1,85 @@ +/// TDesign 弹出层(Popup)组件库。 +/// +/// 对外 API: +/// * [TPopup] — 命令式打开浮层 +/// * [TPopupOptions] — 配置(推荐命名工厂) +/// * [TPopupHandle] — 显隐控制 +/// * [TPopupPlacement]、[TPopupTrigger] — 方向与关闭来源 +/// * [TPopupHeaderBuilder]、[TPopupSlotBuilder]、[TPopupVisibleChangeCallback] — 构建器类型 +library; + +import 'package:flutter/material.dart'; + +import '../../theme/t_colors.dart'; +import '../../theme/t_fonts.dart'; +import '../../theme/t_radius.dart'; +import '../../theme/t_spacers.dart'; +import '../../theme/t_theme.dart'; +import '../../util/context_extension.dart'; +import '../../util/t_toolbar_pressable.dart'; +import '../icon/t_icons.dart'; +import '../text/t_text.dart'; + +part '_popup_center_close.dart'; +part '_popup_header.dart'; +part '_popup_layout.dart'; +part '_popup_route.dart'; +part '_popup_shell.dart'; +part '_popup_tracker.dart'; +part 't_popup_handle.dart'; +part 't_popup_inset.dart'; +part 't_popup_options.dart'; +part 't_popup_types.dart'; + +/// 弹出层入口:五向滑入 / 居中弹出,支持蒙层、bottom 操作区、center 面板外下方关闭区。 +/// +/// 通过 [show] 命令式打开;返回 [TPopupHandle] 用于关闭与再次打开。 +/// 多次调用 [show] 会继续压入新的浮层路由,可用于叠加展示。 +/// +/// **示例** +/// +/// ```dart +/// final handle = TPopup.show( +/// context, +/// options: TPopupOptions.bottom( +/// titleWidget: const Text('标题'), +/// child: MyPanel(), +/// ), +/// ); +/// handle.close(); +/// handle.open(); +/// ``` +/// +/// 配置项见 [TPopupOptions];方向见 [TPopupPlacement]。 +final class TPopup { + const TPopup._(); + + /// 打开浮层并压入独立 [PopupRoute]。 + /// + /// [context] 用于查找 [Navigator] 并展示浮层。 + /// + /// [options] 浮层配置;方向固定时推荐 [TPopupOptions.bottom] 等命名工厂。 + /// + /// 返回 [TPopupHandle],可用 [TPopupHandle.close]、[TPopupHandle.open]、 + /// [TPopupHandle.isShowing] 控制与查询。 + /// 重复调用会继续 push 新的浮层;若需互斥请在业务层管理。 + /// + /// [navigatorContext] 可选,指定承载浮层的 [Navigator] 的 context,默认 [context]。 + /// + /// [useRootNavigator] 为 true 时使用根 [Navigator](嵌套导航场景)。 + static TPopupHandle show( + BuildContext context, { + required TPopupOptions options, + BuildContext? navigatorContext, + bool useRootNavigator = false, + }) { + final navContext = navigatorContext ?? context; + final handle = TPopupHandle._( + options: options, + navigatorContext: navigatorContext, + useRootNavigator: useRootNavigator, + ); + handle.open(navContext); + return handle; + } +} diff --git a/tdesign-component/lib/src/components/popup/t_popup_handle.dart b/tdesign-component/lib/src/components/popup/t_popup_handle.dart new file mode 100644 index 000000000..f64a9bace --- /dev/null +++ b/tdesign-component/lib/src/components/popup/t_popup_handle.dart @@ -0,0 +1,173 @@ +part of 't_popup.dart'; + +/// [TPopup.show] 的返回值,用于控制同一份 [TPopupOptions] 的多次打开与关闭。 +/// +/// **示例** +/// +/// ```dart +/// final handle = TPopup.show( +/// context, +/// options: TPopupOptions.bottom(child: panel), +/// ); +/// handle.close(); +/// handle.open(); // 可省略 context,复用已缓存的 Navigator +/// ``` +class TPopupHandle { + TPopupHandle._({ + required this.options, + this.navigatorContext, + this.useRootNavigator = false, + }); + + /// 创建时传入的配置;每次 [open] 会按 [TPopupOptions.placement] 裁剪无效字段后使用。 + final TPopupOptions options; + + /// 与 [TPopup.show] 的 [navigatorContext] 相同。 + final BuildContext? navigatorContext; + + /// 与 [TPopup.show] 的 [useRootNavigator] 相同。 + final bool useRootNavigator; + + _PopupNavigatorRoute? _route; + NavigatorState? _lastNavigator; + bool _isClosed = false; + int _openEpoch = 0; + + /// 浮层是否仍在展示(路由在栈中且未进入关闭流程)。 + bool get isShowing => _route != null && !_isClosed; + + /// 打开或重新打开浮层。 + /// + /// [context] 可选。首次调用须能解析 [Navigator](传入 [context] 或依赖 + /// [navigatorContext]);后续可省略,优先复用缓存的 [NavigatorState]。 + /// + /// 已展示时调用无副作用。Navigator 已销毁且未提供新 [context] 时,debug 下 assert, + /// release 下静默返回。 + /// + /// 配置非法时会直接抛出 [FlutterError],debug / release 行为一致。 + void open([BuildContext? context]) { + if (isShowing) { + return; + } + final navigator = _resolveNavigator(context); + if (navigator == null) { + assert( + false, + 'TPopupHandle.open: cannot resolve Navigator. ' + 'Either pass a valid context or ensure the handle was created ' + 'with a still-mounted navigatorContext.', + ); + return; + } + + final validationError = options._validatePlacementParams(); + if (validationError != null) { + _throwPopupOptionsValidationError(validationError); + } + final normalized = options.normalized(); + final onClosed = normalized.onClosed; + final openEpoch = ++_openEpoch; + + _isClosed = false; + _lastNavigator = navigator; + + _PopupNavigatorRoute? route; + + void closeWithTrigger(TPopupTrigger trigger) { + final currentRoute = route; + if (!isShowing || currentRoute == null) { + return; + } + _closeRoute( + navigator: navigator, + route: currentRoute, + trigger: trigger, + ); + } + + route = _PopupNavigatorRoute( + options: normalized.copyWith( + onClosed: () { + if (_openEpoch == openEpoch) { + onClosed?.call(); + } + }, + ), + onCloseWithTrigger: closeWithTrigger, + ); + _route = route; + + _PopupTracker.push(navigator, this); + + navigator.push(route).whenComplete(() { + _PopupTracker.remove(navigator, this); + final completedRoute = route; + if (completedRoute != null) { + _detachRoute(completedRoute); + } + }); + } + + /// 关闭当前展示的浮层;[TPopupOptions.onVisibleChange] 的 [TPopupTrigger] 为 + /// [TPopupTrigger.api]。 + /// + /// 已关闭或未展示时调用无副作用。 + /// 嵌套浮层场景下会关闭当前 handle 对应的那一层,而不会误关栈顶其它浮层。 + void close() { + final route = _route; + final navigator = route?.navigator ?? _lastNavigator; + if (!isShowing || route == null || navigator == null) { + return; + } + _closeRoute( + navigator: navigator, + route: route, + trigger: TPopupTrigger.api, + ); + } + + NavigatorState? _resolveNavigator(BuildContext? context) { + final explicitNavigator = _navigatorFromContext(context); + if (explicitNavigator != null) { + return explicitNavigator; + } + final cached = _lastNavigator; + if (cached != null && cached.mounted) { + return cached; + } + return _navigatorFromContext(navigatorContext); + } + + NavigatorState? _navigatorFromContext(BuildContext? context) { + if (context == null || !context.mounted) { + return null; + } + return Navigator.maybeOf(context, rootNavigator: useRootNavigator); + } + + void _markClosing() { + _isClosed = true; + } + + void _closeRoute({ + required NavigatorState navigator, + required _PopupNavigatorRoute route, + required TPopupTrigger trigger, + }) { + _markClosing(); + route.fireCloseStart(trigger); + if (identical(_PopupTracker.top(navigator), this)) { + navigator.pop(); + return; + } + navigator.removeRoute(route); + } + + void _detachRoute(_PopupNavigatorRoute route) { + if (!identical(_route, route)) { + return; + } + _isClosed = true; + _route = null; + } +} diff --git a/tdesign-component/lib/src/components/popup/t_popup_inset.dart b/tdesign-component/lib/src/components/popup/t_popup_inset.dart new file mode 100644 index 000000000..388564acd --- /dev/null +++ b/tdesign-component/lib/src/components/popup/t_popup_inset.dart @@ -0,0 +1,50 @@ +part of 't_popup.dart'; + +/// Popup 在交叉轴方向的边缘留白基类。 +abstract class TPopupInset { + const TPopupInset(); +} + +/// bottom 方向的左右留白。 +class TPopupBottomInset extends TPopupInset { + const TPopupBottomInset({ + this.left = 0, + this.right = 0, + }); + + final double left; + final double right; +} + +/// top 方向的左右留白。 +class TPopupTopInset extends TPopupInset { + const TPopupTopInset({ + this.left = 0, + this.right = 0, + }); + + final double left; + final double right; +} + +/// left 方向的上下留白。 +class TPopupLeftInset extends TPopupInset { + const TPopupLeftInset({ + this.top = 0, + this.bottom = 0, + }); + + final double top; + final double bottom; +} + +/// right 方向的上下留白。 +class TPopupRightInset extends TPopupInset { + const TPopupRightInset({ + this.top = 0, + this.bottom = 0, + }); + + final double top; + final double bottom; +} diff --git a/tdesign-component/lib/src/components/popup/t_popup_options.dart b/tdesign-component/lib/src/components/popup/t_popup_options.dart new file mode 100644 index 000000000..c64623ef0 --- /dev/null +++ b/tdesign-component/lib/src/components/popup/t_popup_options.dart @@ -0,0 +1,656 @@ +part of 't_popup.dart'; + +/// 用于 [TPopupOptions.copyWith] 区分"不传"与"显式 null"。 +const Object _unset = Object(); + +Never _throwPopupOptionsValidationError(String error) { + throw FlutterError('TPopupOptions: $error'); +} + +/// [TPopup.show] 的配置对象。 +/// +/// ## 如何创建 +/// +/// | 场景 | 推荐用法 | +/// |------|----------| +/// | 弹出方向已知 | [TPopupOptions.bottom]、[TPopupOptions.center]、[TPopupOptions.top]、[TPopupOptions.left]、[TPopupOptions.right] | +/// | 方向由变量决定 | 默认构造并设置 [placement];传错字段会在 [TPopup.show] / [TPopupHandle.open] 时抛 [FlutterError] | +/// +/// 命名工厂只暴露当前方向生效的字段(例如 [TPopupOptions.bottom] 无 [width] 参数)。 +/// +/// ## 字段与 [TPopupPlacement] +/// +/// | [TPopupPlacement] | 头部 / 关闭区 | 尺寸 | +/// |-------------------|-------------|------| +/// | [TPopupPlacement.bottom] | [headerBuilder]、[titleWidget]、[cancelBuilder]、[confirmBuilder] | [height]、[inset] | +/// | [TPopupPlacement.center] | [closeBuilder] | [width]、[height] | +/// | [TPopupPlacement.top] | — | [height]、[inset] | +/// | [TPopupPlacement.left]、[TPopupPlacement.right] | — | [width]、[inset] | +/// +/// ## Builder 三态([headerBuilder]、[cancelBuilder]、[confirmBuilder]、[closeBuilder]) +/// +/// | 传参方式 | 效果 | +/// |----------|------| +/// | 省略(使用默认值) | 渲染内置 UI | +/// | 显式 `null` | 隐藏该区域 | +/// | 自定义 [TPopupHeaderBuilder] / [TPopupSlotBuilder] | 完全替换;需自行提供交互与语义,可调用 `close` 关闭浮层 | +/// +/// [titleWidget] 默认为 `null`,表示无标题内容。 +/// +/// 生命周期回调见 [onOpen]、[onOpened]、[onClose]、[onClosed]、[onVisibleChange]、[onOverlayClick]。 +class TPopupOptions { + /// 通用构造;[placement] 在运行时才能确定时使用。 + /// + /// 方向已知时请优先使用 [TPopupOptions.bottom] 等命名工厂。 + const TPopupOptions({ + required this.child, + this.placement = TPopupPlacement.bottom, + this.width, + this.height, + this.inset, + this.radius, + this.backgroundColor, + this.showOverlay = true, + bool? closeOnOverlayClick, + this.overlayColor, + this.overlayOpacity, + this.modal = true, + this.destroyOnClose = false, + this.animationDuration = const Duration(milliseconds: 240), + this.headerBuilder = _kPopupDefaultHeader, + this.titleWidget, + this.cancelBuilder = _kPopupDefaultCancel, + this.confirmBuilder = _kPopupDefaultConfirm, + this.closeBuilder = _kPopupDefaultClose, + this.onOpen, + this.onOpened, + this.onClose, + this.onClosed, + this.onVisibleChange, + this.onOverlayClick, + }) : _closeOnOverlayClick = closeOnOverlayClick; + + /// 创建 [TPopupPlacement.bottom] 配置。 + /// + /// 固定 [placement] 为 [TPopupPlacement.bottom];默认带内置头部。 + /// 蒙层、动画、生命周期等字段语义见同名成员文档。 + factory TPopupOptions.bottom({ + required Widget child, + double? height, + TPopupBottomInset? inset, + TPopupHeaderBuilder? headerBuilder = _kPopupDefaultHeader, + Widget? titleWidget, + TPopupSlotBuilder? cancelBuilder = _kPopupDefaultCancel, + TPopupSlotBuilder? confirmBuilder = _kPopupDefaultConfirm, + double? radius, + Color? backgroundColor, + bool showOverlay = true, + bool? closeOnOverlayClick, + Color? overlayColor, + double? overlayOpacity, + bool modal = true, + bool destroyOnClose = false, + Duration animationDuration = const Duration(milliseconds: 240), + VoidCallback? onOpen, + VoidCallback? onOpened, + VoidCallback? onClose, + VoidCallback? onClosed, + TPopupVisibleChangeCallback? onVisibleChange, + VoidCallback? onOverlayClick, + }) => + TPopupOptions( + child: child, + placement: TPopupPlacement.bottom, + height: height, + inset: inset, + headerBuilder: headerBuilder, + titleWidget: titleWidget, + cancelBuilder: cancelBuilder, + confirmBuilder: confirmBuilder, + radius: radius, + backgroundColor: backgroundColor, + showOverlay: showOverlay, + closeOnOverlayClick: closeOnOverlayClick, + overlayColor: overlayColor, + overlayOpacity: overlayOpacity, + modal: modal, + destroyOnClose: destroyOnClose, + animationDuration: animationDuration, + onOpen: onOpen, + onOpened: onOpened, + onClose: onClose, + onClosed: onClosed, + onVisibleChange: onVisibleChange, + onOverlayClick: onOverlayClick, + ); + + /// 创建 [TPopupPlacement.center] 配置。 + /// + /// 固定 [placement] 为 [TPopupPlacement.center];默认展示面板外下方圆形关闭按钮。 + factory TPopupOptions.center({ + required Widget child, + double? width, + double? height, + TPopupSlotBuilder? closeBuilder = _kPopupDefaultClose, + double? radius, + Color? backgroundColor, + bool showOverlay = true, + bool? closeOnOverlayClick, + Color? overlayColor, + double? overlayOpacity, + bool modal = true, + bool destroyOnClose = false, + Duration animationDuration = const Duration(milliseconds: 240), + VoidCallback? onOpen, + VoidCallback? onOpened, + VoidCallback? onClose, + VoidCallback? onClosed, + TPopupVisibleChangeCallback? onVisibleChange, + VoidCallback? onOverlayClick, + }) => + TPopupOptions( + child: child, + placement: TPopupPlacement.center, + width: width, + height: height, + closeBuilder: closeBuilder, + radius: radius, + backgroundColor: backgroundColor, + showOverlay: showOverlay, + closeOnOverlayClick: closeOnOverlayClick, + overlayColor: overlayColor, + overlayOpacity: overlayOpacity, + modal: modal, + destroyOnClose: destroyOnClose, + animationDuration: animationDuration, + onOpen: onOpen, + onOpened: onOpened, + onClose: onClose, + onClosed: onClosed, + onVisibleChange: onVisibleChange, + onOverlayClick: onOverlayClick, + ); + + /// 创建 [TPopupPlacement.top] 配置。 + /// + /// 固定 [placement] 为 [TPopupPlacement.top];无内置头部。 + factory TPopupOptions.top({ + required Widget child, + double? height, + TPopupTopInset? inset, + double? radius, + Color? backgroundColor, + bool showOverlay = true, + bool? closeOnOverlayClick, + Color? overlayColor, + double? overlayOpacity, + bool modal = true, + bool destroyOnClose = false, + Duration animationDuration = const Duration(milliseconds: 240), + VoidCallback? onOpen, + VoidCallback? onOpened, + VoidCallback? onClose, + VoidCallback? onClosed, + TPopupVisibleChangeCallback? onVisibleChange, + VoidCallback? onOverlayClick, + }) => + TPopupOptions( + child: child, + placement: TPopupPlacement.top, + height: height, + inset: inset, + radius: radius, + backgroundColor: backgroundColor, + showOverlay: showOverlay, + closeOnOverlayClick: closeOnOverlayClick, + overlayColor: overlayColor, + overlayOpacity: overlayOpacity, + modal: modal, + destroyOnClose: destroyOnClose, + animationDuration: animationDuration, + onOpen: onOpen, + onOpened: onOpened, + onClose: onClose, + onClosed: onClosed, + onVisibleChange: onVisibleChange, + onOverlayClick: onOverlayClick, + ); + + /// 创建 [TPopupPlacement.left] 配置。 + /// + /// 固定 [placement] 为 [TPopupPlacement.left];未传 [width] 时布局默认宽度 280。 + factory TPopupOptions.left({ + required Widget child, + double? width, + TPopupLeftInset? inset, + double? radius, + Color? backgroundColor, + bool showOverlay = true, + bool? closeOnOverlayClick, + Color? overlayColor, + double? overlayOpacity, + bool modal = true, + bool destroyOnClose = false, + Duration animationDuration = const Duration(milliseconds: 240), + VoidCallback? onOpen, + VoidCallback? onOpened, + VoidCallback? onClose, + VoidCallback? onClosed, + TPopupVisibleChangeCallback? onVisibleChange, + VoidCallback? onOverlayClick, + }) => + TPopupOptions( + child: child, + placement: TPopupPlacement.left, + width: width, + inset: inset, + radius: radius, + backgroundColor: backgroundColor, + showOverlay: showOverlay, + closeOnOverlayClick: closeOnOverlayClick, + overlayColor: overlayColor, + overlayOpacity: overlayOpacity, + modal: modal, + destroyOnClose: destroyOnClose, + animationDuration: animationDuration, + onOpen: onOpen, + onOpened: onOpened, + onClose: onClose, + onClosed: onClosed, + onVisibleChange: onVisibleChange, + onOverlayClick: onOverlayClick, + ); + + /// 创建 [TPopupPlacement.right] 配置。 + /// + /// 固定 [placement] 为 [TPopupPlacement.right];未传 [width] 时布局默认宽度 280。 + factory TPopupOptions.right({ + required Widget child, + double? width, + TPopupRightInset? inset, + double? radius, + Color? backgroundColor, + bool showOverlay = true, + bool? closeOnOverlayClick, + Color? overlayColor, + double? overlayOpacity, + bool modal = true, + bool destroyOnClose = false, + Duration animationDuration = const Duration(milliseconds: 240), + VoidCallback? onOpen, + VoidCallback? onOpened, + VoidCallback? onClose, + VoidCallback? onClosed, + TPopupVisibleChangeCallback? onVisibleChange, + VoidCallback? onOverlayClick, + }) => + TPopupOptions( + child: child, + placement: TPopupPlacement.right, + width: width, + inset: inset, + radius: radius, + backgroundColor: backgroundColor, + showOverlay: showOverlay, + closeOnOverlayClick: closeOnOverlayClick, + overlayColor: overlayColor, + overlayOpacity: overlayOpacity, + modal: modal, + destroyOnClose: destroyOnClose, + animationDuration: animationDuration, + onOpen: onOpen, + onOpened: onOpened, + onClose: onClose, + onClosed: onClosed, + onVisibleChange: onVisibleChange, + onOverlayClick: onOverlayClick, + ); + + /// 浮层主体内容(必填)。 + final Widget child; + + /// 出现位置,默认 [TPopupPlacement.bottom]。 + final TPopupPlacement placement; + + /// 宽度;[TPopupPlacement.left]、[TPopupPlacement.right]、[TPopupPlacement.center] 生效。 + final double? width; + + /// 高度;[TPopupPlacement.top]、[TPopupPlacement.bottom] 生效;[TPopupPlacement.center] 约束面板尺寸。 + final double? height; + + /// 交叉轴边缘留白;具体类型由 [placement] 决定。 + /// + /// * [TPopupPlacement.bottom] 使用 [TPopupBottomInset] + /// * [TPopupPlacement.top] 使用 [TPopupTopInset] + /// * [TPopupPlacement.left] 使用 [TPopupLeftInset] + /// * [TPopupPlacement.right] 使用 [TPopupRightInset] + /// * [TPopupPlacement.center] 不支持 + final TPopupInset? inset; + + /// 内容区圆角,默认主题大圆角。 + final double? radius; + + /// 内容区背景色,默认主题容器色。 + final Color? backgroundColor; + + /// 是否绘制半透明蒙层。 + /// + /// 当 [modal] 为 true 且此值为 false 时,为“透明模态弹层”。 + final bool showOverlay; + + final bool? _closeOnOverlayClick; + + /// 点击可见蒙层是否关闭。 + /// + /// 省略时默认跟随 [showOverlay]:显示蒙层时为 true,否则为 false。 + bool get closeOnOverlayClick => _closeOnOverlayClick ?? showOverlay; + + /// 蒙层颜色,默认 black54。 + final Color? overlayColor; + + /// 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 + final double? overlayOpacity; + + /// 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 + /// + /// 结合 [showOverlay] 可表达三种模式: + /// * `modal=true, showOverlay=true`:标准模态弹层 + /// * `modal=true, showOverlay=false`:透明模态弹层 + /// * `modal=false, showOverlay=false`:非模态浮层 + final bool modal; + + /// 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 + final bool destroyOnClose; + + /// 打开/关闭动画时长。 + final Duration animationDuration; + + /// bottom 头部;仅 [TPopupPlacement.bottom] 生效。三态见类文档「Builder 三态」。 + /// + /// 自定义时忽略 [titleWidget]、[cancelBuilder]、[confirmBuilder]。 + final TPopupHeaderBuilder? headerBuilder; + + /// bottom 标题插槽;仅 [headerBuilder] 为内置默认时生效。`null` 表示无标题。 + final Widget? titleWidget; + + /// bottom 左侧操作槽;仅 [headerBuilder] 为内置默认时生效。 + /// + /// 内置默认为「取消」,点击触发 [TPopupTrigger.cancel]。 + final TPopupSlotBuilder? cancelBuilder; + + /// bottom 右侧操作槽;仅 [headerBuilder] 为内置默认时生效。 + /// + /// 内置默认为「确定」,点击触发 [TPopupTrigger.confirm]。 + final TPopupSlotBuilder? confirmBuilder; + + /// center 面板外下方关闭区;仅 [TPopupPlacement.center] 生效。三态见类文档「Builder 三态」。 + /// + /// 内置默认点击触发 [TPopupTrigger.close]。 + final TPopupSlotBuilder? closeBuilder; + + /// 路由 push 时(打开动画开始前)。 + final VoidCallback? onOpen; + + /// 打开动画结束。 + final VoidCallback? onOpened; + + /// 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 + final VoidCallback? onClose; + + /// 当前展示周期真正结束。 + /// + /// 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 + final VoidCallback? onClosed; + + /// 显隐变化;第二个参数为 [TPopupTrigger]。 + final TPopupVisibleChangeCallback? onVisibleChange; + + /// 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 + final VoidCallback? onOverlayClick; + + /// 返回配置副本。 + /// + /// 未传入的字段保持原值;对头部/关闭相关插槽显式传入 `null` 表示隐藏该区域。 + TPopupOptions copyWith({ + Widget? child, + TPopupPlacement? placement, + Object? width = _unset, + Object? height = _unset, + Object? inset = _unset, + Object? radius = _unset, + Object? backgroundColor = _unset, + bool? showOverlay, + Object? closeOnOverlayClick = _unset, + Object? overlayColor = _unset, + Object? overlayOpacity = _unset, + bool? modal, + bool? destroyOnClose, + Duration? animationDuration, + Object? headerBuilder = _unset, + Object? titleWidget = _unset, + Object? cancelBuilder = _unset, + Object? confirmBuilder = _unset, + Object? closeBuilder = _unset, + Object? onOpen = _unset, + Object? onOpened = _unset, + Object? onClose = _unset, + Object? onClosed = _unset, + Object? onVisibleChange = _unset, + Object? onOverlayClick = _unset, + }) { + return TPopupOptions( + child: child ?? this.child, + placement: placement ?? this.placement, + width: + identical(width, _unset) ? this.width : (width as num?)?.toDouble(), + height: identical(height, _unset) + ? this.height + : (height as num?)?.toDouble(), + inset: identical(inset, _unset) ? this.inset : inset as TPopupInset?, + radius: identical(radius, _unset) + ? this.radius + : (radius as num?)?.toDouble(), + backgroundColor: identical(backgroundColor, _unset) + ? this.backgroundColor + : backgroundColor as Color?, + showOverlay: showOverlay ?? this.showOverlay, + closeOnOverlayClick: identical(closeOnOverlayClick, _unset) + ? _closeOnOverlayClick + : closeOnOverlayClick as bool?, + overlayColor: identical(overlayColor, _unset) + ? this.overlayColor + : overlayColor as Color?, + overlayOpacity: identical(overlayOpacity, _unset) + ? this.overlayOpacity + : (overlayOpacity as num?)?.toDouble(), + modal: modal ?? this.modal, + destroyOnClose: destroyOnClose ?? this.destroyOnClose, + animationDuration: animationDuration ?? this.animationDuration, + headerBuilder: identical(headerBuilder, _unset) + ? this.headerBuilder + : headerBuilder as TPopupHeaderBuilder?, + titleWidget: identical(titleWidget, _unset) + ? this.titleWidget + : titleWidget as Widget?, + cancelBuilder: identical(cancelBuilder, _unset) + ? this.cancelBuilder + : cancelBuilder as TPopupSlotBuilder?, + confirmBuilder: identical(confirmBuilder, _unset) + ? this.confirmBuilder + : confirmBuilder as TPopupSlotBuilder?, + closeBuilder: identical(closeBuilder, _unset) + ? this.closeBuilder + : closeBuilder as TPopupSlotBuilder?, + onOpen: identical(onOpen, _unset) ? this.onOpen : onOpen as VoidCallback?, + onOpened: identical(onOpened, _unset) + ? this.onOpened + : onOpened as VoidCallback?, + onClose: + identical(onClose, _unset) ? this.onClose : onClose as VoidCallback?, + onClosed: identical(onClosed, _unset) + ? this.onClosed + : onClosed as VoidCallback?, + onVisibleChange: identical(onVisibleChange, _unset) + ? this.onVisibleChange + : onVisibleChange as TPopupVisibleChangeCallback?, + onOverlayClick: identical(onOverlayClick, _unset) + ? this.onOverlayClick + : onOverlayClick as VoidCallback?, + ); + } + + /// {@nodoc} + TPopupOptions normalized() { + final isBottom = placement == TPopupPlacement.bottom; + final isCenter = placement == TPopupPlacement.center; + + return TPopupOptions( + child: child, + placement: placement, + width: width, + height: height, + inset: inset, + radius: radius, + backgroundColor: backgroundColor, + showOverlay: showOverlay, + closeOnOverlayClick: _closeOnOverlayClick, + overlayColor: overlayColor, + overlayOpacity: overlayOpacity, + modal: modal, + destroyOnClose: destroyOnClose, + animationDuration: animationDuration, + headerBuilder: isBottom ? headerBuilder : null, + titleWidget: isBottom ? titleWidget : null, + cancelBuilder: isBottom ? cancelBuilder : null, + confirmBuilder: isBottom ? confirmBuilder : null, + closeBuilder: isCenter ? closeBuilder : null, + onOpen: onOpen, + onOpened: onOpened, + onClose: onClose, + onClosed: onClosed, + onVisibleChange: onVisibleChange, + onOverlayClick: onOverlayClick, + ); + } + + /// {@nodoc} + bool get usesDefaultHeader => _isPopupDefaultHeader(headerBuilder); + + /// {@nodoc} + bool get usesDefaultCancel => _isPopupDefaultCancel(cancelBuilder); + + /// {@nodoc} + bool get usesDefaultConfirm => _isPopupDefaultConfirm(confirmBuilder); + + /// {@nodoc} + bool get usesDefaultClose => _isPopupDefaultClose(closeBuilder); + + /// {@nodoc} + bool get useCustomHeader => + placement == TPopupPlacement.bottom && + headerBuilder != null && + !_isPopupDefaultHeader(headerBuilder); + + /// {@nodoc} + bool get useDefaultHeader => + placement == TPopupPlacement.bottom && + _isPopupDefaultHeader(headerBuilder); + + /// {@nodoc} + bool get showCancelSlot => + placement == TPopupPlacement.bottom && + useDefaultHeader && + cancelBuilder != null; + + /// {@nodoc} + bool get showConfirmSlot => + placement == TPopupPlacement.bottom && + useDefaultHeader && + confirmBuilder != null; + + /// {@nodoc} + bool get hasBuiltInHeader { + if (placement != TPopupPlacement.bottom || headerBuilder == null) { + return false; + } + if (useCustomHeader) { + return true; + } + return cancelBuilder != null || + confirmBuilder != null || + titleWidget != null; + } + + /// {@nodoc} + void assertPlacementParams() { + assert(() { + final err = _validatePlacementParams(); + if (err != null) { + _throwPopupOptionsValidationError(err); + } + return true; + }()); + } + + String? _validatePlacementParams() { + switch (placement) { + case TPopupPlacement.top: + if (width != null) { + return 'width is not valid for placement=top; use height + inset.'; + } + if (inset != null && inset is! TPopupTopInset) { + return 'inset must be TPopupTopInset for placement=top.'; + } + break; + case TPopupPlacement.bottom: + if (width != null) { + return 'width is not valid for placement=bottom; use height + inset.'; + } + if (inset != null && inset is! TPopupBottomInset) { + return 'inset must be TPopupBottomInset for placement=bottom.'; + } + break; + case TPopupPlacement.left: + if (height != null) { + return 'height is not valid for placement=left; use width + inset.'; + } + if (inset != null && inset is! TPopupLeftInset) { + return 'inset must be TPopupLeftInset for placement=left.'; + } + break; + case TPopupPlacement.right: + if (height != null) { + return 'height is not valid for placement=right; use width + inset.'; + } + if (inset != null && inset is! TPopupRightInset) { + return 'inset must be TPopupRightInset for placement=right.'; + } + break; + case TPopupPlacement.center: + if (inset != null) { + return 'inset is not valid for placement=center.'; + } + break; + } + final hasBottomHeaderCustom = !_isPopupDefaultHeader(headerBuilder) || + titleWidget != null || + !_isPopupDefaultCancel(cancelBuilder) || + !_isPopupDefaultConfirm(confirmBuilder); + if (placement != TPopupPlacement.bottom && hasBottomHeaderCustom) { + return 'header/titleWidget/cancel/confirmBuilder only apply to ' + 'placement=bottom (got placement=$placement).'; + } + if (placement != TPopupPlacement.center && + !_isPopupDefaultClose(closeBuilder)) { + return 'closeBuilder only applies to placement=center ' + '(got placement=$placement).'; + } + if (showOverlay && !modal) { + return 'showOverlay=true requires modal=true.'; + } + if (!showOverlay && _closeOnOverlayClick == true) { + return 'closeOnOverlayClick=true requires showOverlay=true.'; + } + return null; + } +} diff --git a/tdesign-component/lib/src/components/popup/t_popup_panel.dart b/tdesign-component/lib/src/components/popup/t_popup_panel.dart deleted file mode 100644 index 9515df905..000000000 --- a/tdesign-component/lib/src/components/popup/t_popup_panel.dart +++ /dev/null @@ -1,622 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import '../../../tdesign_flutter.dart'; -import '../../util/context_extension.dart'; - -typedef PopupClick = Function(); - -/// 弹窗基类 -abstract class TPopupBasePanel extends StatefulWidget { - const TPopupBasePanel({ - Key? key, - required this.child, - this.title, - this.titleColor, - this.backgroundColor, - this.radius, - this.draggable = false, - this.maxHeightRatio = 0.9, - this.minHeightRatio = 0.3, - this.fixedHeight, - }) : super(key: key); - - /// 子控件 - final Widget child; - - /// 标题 - final String? title; - - /// 标题颜色 - final Color? titleColor; - - /// 背景颜色 - final Color? backgroundColor; - - /// 圆角 - final double? radius; - - /// 边缘是否可拖动 - final bool draggable; - - /// 最大高度比例 - final double maxHeightRatio; - - /// 最小高度比例 - final double minHeightRatio; - - /// 固定高度(px)。设置后忽略 [maxHeightRatio] / [minHeightRatio], - /// 面板高度固定为该值。 - final double? fixedHeight; - - @override - State createState(); -} - -abstract class _TPopupBaseState extends State - with SingleTickerProviderStateMixin { - final GlobalKey _childKey = GlobalKey(); - static const _dragHandleHeight = 24.0; - static const _headerHeight = 58.0; - - late AnimationController _controller; - double _maxHeight = 0; - double _minHeight = 0; - double _currentHeight = 0; - double _lastScreenHeight = 0; - double? _lastMaxHeightRatio; - double? _lastMinHeightRatio; - bool? _lastDraggable; - bool _isFullscreen = false; - bool _isAnimating = false; - bool _isDragging = false; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - duration: const Duration(milliseconds: 400), - vsync: this, - ); - if (widget.draggable) { - _controller.addListener(_updateHeight); - } - WidgetsBinding.instance.addPostFrameCallback((_) => _initHeight()); - } - - /// 根据屏幕高度和比例(或固定高度)初始化面板高度 - void _initHeight() { - final ctx = _childKey.currentContext ?? context; - final screenHeight = MediaQuery.of(ctx).size.height; - - if (_lastScreenHeight == screenHeight && - _lastMaxHeightRatio == widget.maxHeightRatio && - _lastMinHeightRatio == widget.minHeightRatio && - _lastDraggable == widget.draggable) { - return; - } - _lastScreenHeight = screenHeight; - _lastMaxHeightRatio = widget.maxHeightRatio; - _lastMinHeightRatio = widget.minHeightRatio; - _lastDraggable = widget.draggable; - - if (widget.fixedHeight != null) { - _maxHeight = widget.fixedHeight!; - _minHeight = widget.fixedHeight!; - } else { - _maxHeight = screenHeight * widget.maxHeightRatio; - _minHeight = screenHeight * widget.minHeightRatio; - if (_minHeight > _maxHeight) { - _minHeight = _maxHeight; - } - } - - _currentHeight = _maxHeight; - _controller.value = 1.0; - // 触发 rebuild 以应用新计算的 _currentHeight - setState(() {}); - } - - void _updateHeight() => setState(() { - _currentHeight = - _minHeight + (_maxHeight - _minHeight) * _controller.value; - }); - - void _toggleFullscreen(bool fullscreen) { - if (_isAnimating || _isFullscreen == fullscreen) { - return; - } - - setState(() { - _isFullscreen = fullscreen; - _maxHeight = fullscreen - ? MediaQuery.of(context).size.height - : MediaQuery.of(context).size.height * widget.maxHeightRatio; - }); - - _controller.animateTo( - fullscreen ? 1.0 : 0.0, - duration: const Duration(milliseconds: 350), - curve: Curves.fastOutSlowIn, - ); - } - - void _animateTo(double height) { - if (_isAnimating) { - return; - } - _isAnimating = true; - - final value = (height - _minHeight) / (_maxHeight - _minHeight); - _controller - .animateTo( - value.clamp(0.0, 1.0), - duration: const Duration(milliseconds: 300), - curve: Curves.easeOutBack, - ) - .whenComplete(() => _isAnimating = false); - } - - Widget _buildDragHandle() { - if (!widget.draggable) { - return const SizedBox.shrink(); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onVerticalDragUpdate: _handleDragUpdate, - onVerticalDragEnd: _handleDragEnd, - onDoubleTap: () => _toggleFullscreen(!_isFullscreen), - child: Container( - height: _dragHandleHeight, - alignment: Alignment.center, - child: Container( - width: 48, - height: 4, - decoration: BoxDecoration( - color: TTheme.of(context).componentStrokeColor, - borderRadius: BorderRadius.circular(2), - ), - ), - ), - ); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted || _isDragging) { - return; - } - _initHeight(); - }); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _controller, - builder: (context, _) => RepaintBoundary( - child: Container( - height: _currentHeight, - decoration: BoxDecoration( - color: - widget.backgroundColor ?? TTheme.of(context).bgColorContainer, - borderRadius: _isFullscreen - ? null - : BorderRadius.vertical( - top: Radius.circular( - widget.radius ?? TTheme.of(context).radiusExtraLarge)), - ), - child: Column(children: [ - _buildDragHandle(), - buildHeader(context), - Expanded( - child: _buildContent(), - ), - ]), - ), - ), - ); - } - - Widget _buildContent() => NotificationListener( - onNotification: (notification) { - if (notification is ScrollUpdateNotification) { - final metrics = notification.metrics; - if ((metrics.pixels <= 0 || - metrics.pixels >= metrics.maxScrollExtent) && - notification.dragDetails != null) { - _handleDragUpdate(notification.dragDetails!); - } - } - return false; - }, - child: Container( - key: _childKey, - child: widget.child, - ), - ); - - @protected - void _handleDragUpdate(DragUpdateDetails details); - - @protected - void _handleDragEnd(DragEndDetails details); - - @protected - Widget buildHeader(BuildContext context); - - void _baseHandleDragUpdate(DragUpdateDetails details) { - _isDragging = true; - if (_isAnimating || !widget.draggable) { - return; - } - - final newHeight = _currentHeight - details.primaryDelta! * 1.2; - _currentHeight = newHeight.clamp(_minHeight, _maxHeight); - _controller.value = - (_currentHeight - _minHeight) / (_maxHeight - _minHeight); - } - - void _baseHandleDragEnd(DragEndDetails details) { - final velocity = details.velocity.pixelsPerSecond.dy; - final predictedHeight = _currentHeight + velocity * 0.15; - - if (predictedHeight > _maxHeight * 0.7 || velocity < -800) { - _animateTo(_maxHeight); - } else if (predictedHeight < _minHeight * 1.3 || velocity > 800) { - _animateTo(_minHeight); - } - _isDragging = false; - } -} - -/// 右上角带关闭的底部浮层面板 -class TPopupBottomDisplayPanel extends TPopupBasePanel { - const TPopupBottomDisplayPanel({ - super.key, - required super.child, - super.title, - super.titleColor, - this.titleFontSize, - this.titleLeft = false, - this.hideClose = false, - this.closeColor, - this.closeSize, - this.closeClick, - super.backgroundColor, - super.radius, - super.draggable, - super.maxHeightRatio, - super.minHeightRatio, - super.fixedHeight, - }); - - /// 标题字体大小 - final double? titleFontSize; - - /// 标题是否靠左 - final bool titleLeft; - - /// 是否隐藏关闭按钮 - final bool hideClose; - - /// 关闭按钮颜色 - final Color? closeColor; - - /// 关闭按钮图标尺寸 - final double? closeSize; - - /// 关闭按钮点击回调 - final PopupClick? closeClick; - - @override - State createState() => _TPopupBottomDisplayPanelState(); -} - -class _TPopupBottomDisplayPanelState - extends _TPopupBaseState { - @override - Widget buildHeader(BuildContext context) { - Widget header = Container( - alignment: widget.titleLeft ? Alignment.centerLeft : Alignment.center, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TText( - widget.title ?? '', - textColor: widget.titleColor ?? TTheme.of(context).textColorPrimary, - font: TTheme.of(context).fontTitleLarge?.withSize( - widget.titleFontSize?.toInt() ?? - TTheme.of(context).fontTitleLarge!.size.toInt()), - fontWeight: FontWeight.w700, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ); - - if (!widget.hideClose) { - header = Stack( - alignment: Alignment.centerLeft, - children: [ - Padding( - padding: - EdgeInsets.only(right: 40, left: widget.titleLeft ? 0 : 40), - child: header, - ), - Positioned( - right: 0, - child: IconButton( - icon: Icon( - TIcons.close, - color: widget.closeColor, - size: widget.closeSize, - ), - onPressed: widget.closeClick, - ), - ), - ], - ); - } - - return SizedBox( - height: widget.draggable - ? _TPopupBaseState._headerHeight - - _TPopupBaseState._dragHandleHeight - : _TPopupBaseState._headerHeight, - child: header, - ); - } - - @override - void _handleDragUpdate(DragUpdateDetails details) { - super._baseHandleDragUpdate(details); - - final progress = (_currentHeight - _minHeight) / (_maxHeight - _minHeight); - if (progress > 0.85 && !_isFullscreen) { - _toggleFullscreen(true); - } else if (progress < 0.75 && _isFullscreen) { - _toggleFullscreen(false); - } - } - - @override - void _handleDragEnd(DragEndDetails details) => - super._baseHandleDragEnd(details); -} - -/// 带确认的底部浮层面板 -class TPopupBottomConfirmPanel extends TPopupBasePanel { - const TPopupBottomConfirmPanel({ - super.key, - required super.child, - super.title, - super.titleColor, - this.leftText, - this.leftTextColor, - this.leftClick, - this.rightText, - this.rightTextColor, - this.rightClick, - this.titleFontSize, - this.leftTextFontSize, - this.rightTextFontSize, - super.backgroundColor, - super.radius, - super.draggable, - super.maxHeightRatio, - super.minHeightRatio, - }); - - /// 标题字体大小 - final double? titleFontSize; - - /// 左边文本 - final String? leftText; - - /// 左边文本字体大小 - final double? leftTextFontSize; - - /// 左边文本颜色 - final Color? leftTextColor; - - /// 左边文本点击回调 - final PopupClick? leftClick; - - /// 右边文本 - final String? rightText; - - /// 右边文本字体大小 - final double? rightTextFontSize; - - /// 右边文本颜色 - final Color? rightTextColor; - - /// 右边文本点击回调 - final PopupClick? rightClick; - - @override - State createState() => _TPopupBottomConfirmPanelState(); -} - -class _TPopupBottomConfirmPanelState - extends _TPopupBaseState { - @override - Widget buildHeader(BuildContext context) { - return SizedBox( - height: widget.draggable - ? _TPopupBaseState._headerHeight - - _TPopupBaseState._dragHandleHeight - : _TPopupBaseState._headerHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildActionButton( - text: widget.leftText ?? context.resource.cancel, - color: - widget.leftTextColor ?? TTheme.of(context).textColorSecondary, - onTap: widget.leftClick, - left: true, - ), - Expanded( - child: Center( - child: TText( - widget.title ?? '', - textColor: - widget.titleColor ?? TTheme.of(context).textColorPrimary, - font: TTheme.of(context).fontTitleLarge?.withSize( - widget.titleFontSize?.toInt() ?? - TTheme.of(context).fontTitleLarge!.size.toInt()), - fontWeight: FontWeight.w700, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - _buildActionButton( - text: widget.rightText ?? context.resource.confirm, - color: - widget.rightTextColor ?? TTheme.of(context).brandNormalColor, - onTap: widget.rightClick, - left: false, - ), - ], - ), - ); - } - - Widget _buildActionButton({ - required String text, - required Color color, - required VoidCallback? onTap, - required bool left, - }) => - GestureDetector( - onTap: onTap, - child: Padding( - padding: EdgeInsets.only( - left: left ? 16 : 0, - right: left ? 0 : 16, - ), - child: TText( - text, - textColor: color, - font: (left - ? TTheme.of(context).fontBodyLarge - : TTheme.of(context).fontTitleMedium) - ?.withSize(left - ? widget.leftTextFontSize?.toInt() ?? - TTheme.of(context).fontBodyLarge!.size.toInt() - : widget.rightTextFontSize?.toInt() ?? - TTheme.of(context).fontTitleMedium!.size.toInt()), - fontWeight: left ? FontWeight.w400 : FontWeight.w600, - ), - ), - ); - - @override - void _handleDragUpdate(DragUpdateDetails details) { - super._baseHandleDragUpdate(details); - - const threshold = 0.15; - final progress = (_currentHeight - _minHeight) / (_maxHeight - _minHeight); - if (progress > (1 - threshold) && !_isFullscreen) { - _toggleFullscreen(true); - } else if (progress < threshold && _isFullscreen) { - _toggleFullscreen(false); - } - } - - @override - void _handleDragEnd(DragEndDetails details) => - super._baseHandleDragEnd(details); -} - -/// 居中浮层面板 -class TPopupCenterPanel extends StatelessWidget { - const TPopupCenterPanel({ - super.key, - required this.child, - this.closeUnderBottom = false, - this.closeColor, - this.closeClick, - this.backgroundColor, - this.radius, - this.closeSize, - }); - - /// 子控件 - final Widget child; - - /// 关闭按钮是否在视图框下方 - final bool closeUnderBottom; - - /// 关闭按钮颜色 - final Color? closeColor; - - /// 关闭按钮图标尺寸 - final double? closeSize; - - /// 关闭按钮点击回调 - final PopupClick? closeClick; - - /// 背景颜色 - final Color? backgroundColor; - - /// 圆角 - final double? radius; - - @override - Widget build(BuildContext context) { - if (closeUnderBottom) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 40), - Container( - margin: const EdgeInsets.symmetric(vertical: 24), - decoration: BoxDecoration( - color: backgroundColor ?? TTheme.of(context).bgColorContainer, - borderRadius: BorderRadius.circular( - radius ?? TTheme.of(context).radiusExtraLarge), - ), - child: child, - ), - IconButton( - icon: Icon( - TIcons.close_circle, - color: closeColor ?? TTheme.of(context).fontWhColor1, - size: closeSize ?? 32, - ), - onPressed: closeClick, - ), - ], - ); - } - - return Container( - decoration: BoxDecoration( - color: backgroundColor ?? TTheme.of(context).bgColorContainer, - borderRadius: BorderRadius.circular( - radius ?? TTheme.of(context).radiusExtraLarge), - ), - child: Stack( - children: [ - child, - Positioned( - top: TTheme.of(context).spacer8, - right: TTheme.of(context).spacer8, - child: IconButton( - icon: Icon( - TIcons.close, - color: closeColor, - size: closeSize, - ), - onPressed: closeClick, - ), - ), - ], - ), - ); - } -} diff --git a/tdesign-component/lib/src/components/popup/t_popup_route.dart b/tdesign-component/lib/src/components/popup/t_popup_route.dart deleted file mode 100644 index 226f6eaf7..000000000 --- a/tdesign-component/lib/src/components/popup/t_popup_route.dart +++ /dev/null @@ -1,300 +0,0 @@ -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); - -/// 从屏幕弹出的方向 -enum SlideTransitionFrom { top, right, left, bottom, center } - -/// 从屏幕的某个方向滑动弹出的Dialog框的路由,比如从顶部、底部、左、右滑出页面 -class TSlidePopupRoute extends PopupRoute { - TSlidePopupRoute({ - required this.builder, - this.barrierLabel, - this.modalBarrierColor = Colors.black54, - this.isDismissible = true, - this.modalBarrierFull = false, - this.slideTransitionFrom = SlideTransitionFrom.bottom, - this.modalWidth, - this.modalHeight, - this.modalTop = 0, - this.modalLeft = 0, - this.open, - this.opened, - this.close, - this.barrierClick, - this.focusMove = false, - }); - - /// 控件构建器 - final WidgetBuilder builder; - - /// 蒙层颜色 - final Color? modalBarrierColor; - - /// 点击蒙层能否关闭 - final bool isDismissible; - - /// 是否全屏显示蒙层 - final bool modalBarrierFull; - - /// 设置从屏幕的哪个方向滑出 - final SlideTransitionFrom slideTransitionFrom; - - /// 弹出框宽度 - final double? modalWidth; - - /// 弹出框高度 - final double? modalHeight; - - /// 弹出框顶部距离 - final double? modalTop; - - /// 弹出框左侧距离 - final double? modalLeft; - - /// 打开前事件 - final VoidCallback? open; - - /// 打开后事件 - final VoidCallback? opened; - - /// 关闭前事件 - final VoidCallback? close; - - /// 蒙层点击事件,仅在[modalBarrierFull]为false时触发 - final VoidCallback? barrierClick; - - /// 是否有输入框获取焦点时整体平移避免输入框被遮挡 - final bool focusMove; - - Color get _barrierColor => modalBarrierColor ?? Colors.black54; - - @override - Duration get transitionDuration => _bottomSheetEnterDuration; - - @override - Duration get reverseTransitionDuration => _bottomSheetExitDuration; - - @override - bool get barrierDismissible => isDismissible; - - @override - final String? barrierLabel; - - @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( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, - ) { - var animValue = decelerateEasing.transform(animation.value); - return Stack( - children: [ - if (!modalBarrierFull) - _getPositionWidget( - context, - IgnorePointer( - ignoring: true, - child: Container( - 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); - } - }, - ), - ), - ), - ), - _getPositionWidget( - context, - Align( - alignment: slideTransitionFromToAlignment(slideTransitionFrom), - child: slideTransitionFrom != SlideTransitionFrom.center - ? FractionalTranslation( - translation: _getOffset(animValue, slideTransitionFrom), - child: ClipRect( - clipper: RectClipper(animValue, slideTransitionFrom), - child: child, - ), - ) - : Transform( - transform: Matrix4.diagonal3Values(animValue, animValue, 1), - alignment: Alignment.center, - child: child, - ), - ), - ), - ], - ); - } - - @override - Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { - return Material( - child: builder.call(context), - color: Colors.transparent, - ); - } - - @override - TickerFuture didPush() { - startFocusListener(navigator!.context); - open?.call(); - animation?.addStatusListener((status) { - if (status == AnimationStatus.completed) { - opened?.call(); - } - }); - return super.didPush(); - } - - @override - void dispose() { - 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(); - } - - @override - bool didPop(T? result) { - close?.call(); - return super.didPop(result); - } - - Widget _getPositionWidget(BuildContext context, Widget child) { - 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() - (focusMove ? bottom : 0); - 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(); - return Positioned( - top: _modalTop, - bottom: screenSize.height - _modalTop - _modalHeight, - left: _modalLeft, - right: screenSize.width - _modalLeft - _modalWidth, - child: child, - ); - } - - Offset _getOffset(double animValue, SlideTransitionFrom slideTransitionFrom) { - switch (slideTransitionFrom) { - case SlideTransitionFrom.top: - return Offset(0, animValue - 1); - case SlideTransitionFrom.right: - return Offset(1 - animValue, 0); - case SlideTransitionFrom.left: - return Offset(animValue - 1, 0); - case SlideTransitionFrom.bottom: - return Offset(0, 1 - animValue); - default: - return const Offset(0, 0); - } - } -} - -Alignment slideTransitionFromToAlignment(SlideTransitionFrom from) { - switch (from) { - case SlideTransitionFrom.top: - return Alignment.topCenter; - case SlideTransitionFrom.right: - return Alignment.centerRight; - case SlideTransitionFrom.left: - return Alignment.centerLeft; - case SlideTransitionFrom.bottom: - return Alignment.bottomCenter; - case SlideTransitionFrom.center: - return Alignment.center; - } -} - -class RectClipper extends CustomClipper { - final double animValue; - final SlideTransitionFrom slideTransitionFrom; - - RectClipper(this.animValue, this.slideTransitionFrom); - - @override - Rect getClip(Size size) { - switch (slideTransitionFrom) { - case SlideTransitionFrom.top: - return Rect.fromLTWH(0, size.height * (1 - animValue), size.width, size.height); - case SlideTransitionFrom.right: - return Rect.fromLTWH(0, 0, size.width * animValue, size.height); - case SlideTransitionFrom.left: - return Rect.fromLTWH(size.width * (1 - animValue), 0, size.width, size.height); - case SlideTransitionFrom.bottom: - return Rect.fromLTWH(0, 0, size.width, size.height * animValue); - default: - return Rect.fromLTWH(0, 0, size.width, size.height); - } - } - - @override - bool shouldReclip(covariant CustomClipper oldClipper) { - return oldClipper != this; - } -} diff --git a/tdesign-component/lib/src/components/popup/t_popup_types.dart b/tdesign-component/lib/src/components/popup/t_popup_types.dart new file mode 100644 index 000000000..668ccf7cc --- /dev/null +++ b/tdesign-component/lib/src/components/popup/t_popup_types.dart @@ -0,0 +1,109 @@ +part of 't_popup.dart'; + +/// 浮层出现方向;决定 [TPopupOptions] 中哪些字段生效。 +/// +/// 与 [TPopupOptions] 类文档中的「字段与 placement」表对应。 +/// 方向固定时请用 [TPopupOptions.bottom]、[TPopupOptions.center] 等命名工厂。 +enum TPopupPlacement { + /// 自顶部滑入;使用 [TPopupOptions.height]、[TPopupOptions.inset]([TPopupTopInset])。 + top, + + /// 自左侧滑入;使用 [TPopupOptions.width]、[TPopupOptions.inset]([TPopupLeftInset])。 + left, + + /// 自右侧滑入;使用 [TPopupOptions.width]、[TPopupOptions.inset]([TPopupRightInset])。 + right, + + /// 自底部滑入;默认内置头部;使用 [TPopupOptions.height]、[TPopupOptions.inset]([TPopupBottomInset])。 + bottom, + + /// 屏幕居中;使用 [TPopupOptions.closeBuilder] 控制面板外下方关闭区。 + center, +} + +/// bottom 整行头部自定义构建器。 +/// +/// * [context] 构建上下文 +/// * [close] 关闭浮层,触发源为 [TPopupTrigger.custom] +typedef TPopupHeaderBuilder = Widget Function( + BuildContext context, + VoidCallback close, +); + +/// bottom 左右操作槽或 center 关闭区构建器。 +/// +/// * [context] 构建上下文 +/// * [close] 关闭浮层;触发源与槽位语义保持一致 +/// +/// 自定义 builder 需自行提供交互与无障碍语义;框架仅为内置默认控件补充默认语义。 +typedef TPopupSlotBuilder = Widget Function( + BuildContext context, + VoidCallback close, +); + +// 库内 sentinel:识别 builder「未传 = 内置默认」。业务三态见 [TPopupOptions]。 + +Widget _kPopupDefaultHeader(BuildContext context, VoidCallback close) => + const SizedBox.shrink(); + +Widget _kPopupDefaultCancel(BuildContext context, VoidCallback close) => + const SizedBox.shrink(); + +Widget _kPopupDefaultConfirm(BuildContext context, VoidCallback close) => + const SizedBox.shrink(); + +Widget _kPopupDefaultClose(BuildContext context, VoidCallback close) => + const SizedBox.shrink(); + +bool _isPopupDefaultHeader(TPopupHeaderBuilder? builder) => + identical(builder, _kPopupDefaultHeader); + +bool _isPopupDefaultCancel(TPopupSlotBuilder? builder) => + identical(builder, _kPopupDefaultCancel); + +bool _isPopupDefaultConfirm(TPopupSlotBuilder? builder) => + identical(builder, _kPopupDefaultConfirm); + +bool _isPopupDefaultClose(TPopupSlotBuilder? builder) => + identical(builder, _kPopupDefaultClose); + +/// 浮层关闭或显隐变化时的触发来源。 +/// +/// 作为 [TPopupVisibleChangeCallback] 的第二个参数,以及关闭流程中的语义标记。 +/// +/// 内置控件会映射为 [TPopupTrigger.overlay]、[TPopupTrigger.cancel]、 +/// [TPopupTrigger.confirm]、[TPopupTrigger.close]; +/// [TPopupHandle.close] 为 [TPopupTrigger.api];系统返回为 +/// [TPopupTrigger.systemBack];[headerBuilder] 内调用 `close` 等为 +/// [TPopupTrigger.custom]。 +enum TPopupTrigger { + /// 点击蒙层,且 [TPopupOptions.closeOnOverlayClick] 为 true。 + overlay, + + /// 点击 bottom 取消语义槽位(含默认与自定义 [TPopupOptions.cancelBuilder])。 + cancel, + + /// 点击 bottom 确认语义槽位(含默认与自定义 [TPopupOptions.confirmBuilder])。 + confirm, + + /// 点击 center 关闭语义槽位(含默认与自定义 [TPopupOptions.closeBuilder])。 + close, + + /// 外部 API 主动触发的显隐变化,如 [TPopupHandle.close] 或打开事件。 + api, + + /// 系统返回键或系统路由返回触发的关闭。 + systemBack, + + /// 无框架预设动作语义的自定义关闭,如 [headerBuilder] 内调用 `close`。 + custom, +} + +/// 浮层显隐变化回调。 +/// +/// * [visible] 为 true 表示打开,false 表示开始关闭 +/// * [trigger] 关闭来源,见 [TPopupTrigger];打开时为 [TPopupTrigger.api] +typedef TPopupVisibleChangeCallback = void Function( + bool visible, + TPopupTrigger trigger, +); diff --git a/tdesign-component/lib/src/util/t_toolbar_pressable.dart b/tdesign-component/lib/src/util/t_toolbar_pressable.dart new file mode 100644 index 000000000..7ee6d2de3 --- /dev/null +++ b/tdesign-component/lib/src/util/t_toolbar_pressable.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; + +import '../theme/t_spacers.dart'; +import '../theme/t_theme.dart'; + +/// 工具栏文字/图标按钮统一按压反馈:按下时整体透明度动画。 +/// +/// 用于 [TPicker]、[TPopup] 等「取消 | 标题 | 确认」类工具栏,后续组件请复用。 +class TToolbarPressable extends StatefulWidget { + const TToolbarPressable({ + super.key, + required this.child, + this.onTap, + this.padding, + this.enabled = true, + this.pressDuration = kToolbarPressDuration, + this.pressedOpacity = kToolbarPressedOpacity, + this.mergeTextStyle, + this.mergeIconTheme, + }); + + /// 按压动画时长(与 TDesign 工具栏规范一致)。 + static const Duration kToolbarPressDuration = Duration(milliseconds: 100); + + /// 按下时的目标透明度。 + static const double kToolbarPressedOpacity = 0.5; + + final Widget child; + final VoidCallback? onTap; + final EdgeInsetsGeometry? padding; + final bool enabled; + final Duration pressDuration; + final double pressedOpacity; + + /// 为子树 [Text] 提供默认样式(merge 语义,子控件已有样式优先)。 + final TextStyle? mergeTextStyle; + + /// 为子树 [Icon] 提供默认样式。 + final IconThemeData? mergeIconTheme; + + @override + State createState() => _TToolbarPressableState(); +} + +class _TToolbarPressableState extends State { + bool _pressed = false; + + void _setPressed(bool value) { + if (!widget.enabled || widget.onTap == null) { + return; + } + if (_pressed == value) { + return; + } + setState(() => _pressed = value); + } + + @override + Widget build(BuildContext context) { + final theme = TTheme.of(context); + final padding = widget.padding ?? + EdgeInsets.symmetric( + horizontal: theme.spacer8, + vertical: theme.spacer12, + ); + + Widget child = widget.child; + if (widget.mergeTextStyle != null) { + child = DefaultTextStyle.merge( + style: widget.mergeTextStyle!, + child: child, + ); + } + if (widget.mergeIconTheme != null) { + child = IconTheme.merge( + data: widget.mergeIconTheme!, + child: child, + ); + } + + final enabled = widget.enabled && widget.onTap != null; + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTapDown: enabled ? (_) => _setPressed(true) : null, + onTapUp: enabled ? (_) => _setPressed(false) : null, + onTapCancel: enabled ? () => _setPressed(false) : null, + onTap: enabled ? widget.onTap : null, + child: AnimatedOpacity( + duration: widget.pressDuration, + opacity: _pressed ? widget.pressedOpacity : 1, + child: Padding(padding: padding, child: child), + ), + ); + } +} diff --git a/tdesign-component/lib/tdesign_flutter.dart b/tdesign-component/lib/tdesign_flutter.dart index 02701bef7..7a004cbd6 100644 --- a/tdesign-component/lib/tdesign_flutter.dart +++ b/tdesign-component/lib/tdesign_flutter.dart @@ -55,8 +55,21 @@ export 'src/components/picker/t_picker_option.dart'; export 'src/components/picker/t_picker_value.dart'; export 'src/components/popover/t_popover.dart'; export 'src/components/popover/t_popover_widget.dart'; -export 'src/components/popup/t_popup_panel.dart'; -export 'src/components/popup/t_popup_route.dart'; +export 'src/components/popup/t_popup.dart' + show + TPopup, + TPopupHandle, + TPopupOptions, + TPopupPlacement, + TPopupTrigger, + TPopupInset, + TPopupTopInset, + TPopupBottomInset, + TPopupLeftInset, + TPopupRightInset, + TPopupHeaderBuilder, + TPopupSlotBuilder, + TPopupVisibleChangeCallback; export 'src/components/progress/t_progress.dart'; export 'src/components/radio/t_radio.dart'; export 'src/components/rate/t_rate.dart'; @@ -108,3 +121,4 @@ export 'src/theme/t_shadows.dart'; export 'src/theme/t_spacers.dart'; export 'src/theme/t_theme.dart'; export 'src/util/platform_util.dart'; +export 'src/util/t_toolbar_pressable.dart'; diff --git a/tdesign-component/test/helpers/popup_test_helpers.dart b/tdesign-component/test/helpers/popup_test_helpers.dart new file mode 100644 index 000000000..702cceb1d --- /dev/null +++ b/tdesign-component/test/helpers/popup_test_helpers.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +import 'popup_test_resource.dart'; + +Widget wrapPopupTest( + Widget child, { + PopupTestResourceDelegate? resource, +}) { + final resolved = resource ?? PopupTestResourceDelegate.zh(); + bindPopupTestResource(resolved); + return MaterialApp( + locale: resolved.locale, + home: TTheme( + data: TThemeData.defaultData(), + child: Scaffold(body: child), + ), + ); +} + +Future openPopup( + WidgetTester tester, { + required VoidCallback onPressed, + PopupTestResourceDelegate? resource, +}) async { + await tester.pumpWidget( + wrapPopupTest( + Builder( + builder: (context) => ElevatedButton( + onPressed: onPressed, + child: const Text('open'), + ), + ), + resource: resource ?? PopupTestResourceDelegate.zh(), + ), + ); + await tester.tap(find.text('open')); +} diff --git a/tdesign-component/test/helpers/popup_test_resource.dart b/tdesign-component/test/helpers/popup_test_resource.dart new file mode 100644 index 000000000..2537875d9 --- /dev/null +++ b/tdesign-component/test/helpers/popup_test_resource.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +/// Popup 测试用文案资源:仅覆盖 cancel / confirm,其余与库内默认中文一致。 +class PopupTestResourceDelegate extends TResourceDelegate { + PopupTestResourceDelegate({ + required this.cancelText, + required this.confirmText, + required this.locale, + }); + + factory PopupTestResourceDelegate.zh() => PopupTestResourceDelegate( + cancelText: '取消', + confirmText: '确定', + locale: const Locale('zh'), + ); + + factory PopupTestResourceDelegate.en() => PopupTestResourceDelegate( + cancelText: 'Cancel', + confirmText: 'Confirm', + locale: const Locale('en'), + ); + + final String cancelText; + final String confirmText; + final Locale locale; + + @override + String get cancel => cancelText; + + @override + String get confirm => confirmText; + + @override + String get open => '开'; + + @override + String get close => '关'; + + @override + String get badgeZero => '0'; + + @override + String get other => '其它'; + + @override + String get reset => '重置'; + + @override + String get loading => '加载中'; + + @override + String get loadingWithPoint => '加载中...'; + + @override + String get knew => '知道了'; + + @override + String get refreshing => '正在刷新'; + + @override + String get releaseRefresh => '松开刷新'; + + @override + String get pullToRefresh => '下拉刷新'; + + @override + String get completeRefresh => '刷新完成'; + + @override + String get days => '天'; + + @override + String get hours => '时'; + + @override + String get minutes => '分'; + + @override + String get seconds => '秒'; + + @override + String get milliseconds => '毫秒'; + + @override + String get yearLabel => '年'; + + @override + String get monthLabel => '月'; + + @override + String get dateLabel => '日'; + + @override + String get weeksLabel => '周'; + + @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 => '结束'; + + @override + String get notRated => '未评分'; + + @override + String get cascadeLabel => '选择选项'; + + @override + String get back => '返回'; + + @override + String get top => '顶部'; + + @override + String get emptyData => '暂无数据'; +} + +/// 在测试中注入 [resource];与业务侧 `TTheme.setResourceBuilder` 用法一致。 +void bindPopupTestResource(PopupTestResourceDelegate resource) { + TTheme.setResourceBuilder((_) => resource, needAlwaysBuild: true); +} + +/// 恢复为库内默认资源(中文 cancel/confirm)。 +void resetPopupTestResource() { + TTheme.setResourceBuilder((_) => null, needAlwaysBuild: false); +} diff --git a/tdesign-component/test/t_bottom_tab_bar_test.dart b/tdesign-component/test/t_bottom_tab_bar_test.dart index 99cd97514..b64934bcf 100644 --- a/tdesign-component/test/t_bottom_tab_bar_test.dart +++ b/tdesign-component/test/t_bottom_tab_bar_test.dart @@ -64,7 +64,8 @@ void main() { group('TBottomTabBar — iconText 图标颜色 (issue #900)', () { // TC-01: iconText 选中 tab 图标颜色为 brandNormalColor testWidgets('TC-01: 选中 tab 的图标颜色应为 brandNormalColor', (tester) async { - await tester.pumpWidget(_buildTestApp(_buildIconTextTabBar(currentIndex: 0))); + await tester + .pumpWidget(_buildTestApp(_buildIconTextTabBar(currentIndex: 0))); await tester.pumpAndSettle(); final BuildContext context = tester.element(find.byType(TBottomTabBar)); @@ -75,7 +76,8 @@ void main() { expect(icons, isNotEmpty); // 通过 IconTheme 验证选中图标的颜色 - final iconThemes = tester.widgetList(find.byType(IconTheme)).toList(); + final iconThemes = + tester.widgetList(find.byType(IconTheme)).toList(); expect(iconThemes, isNotEmpty, reason: '选中图标应被 IconTheme 包裹以注入颜色'); // 验证至少有一个 IconTheme 的 color 是 brandNormalColor @@ -91,13 +93,15 @@ void main() { // TC-02: iconText 未选中 tab 图标颜色为 textColorPrimary testWidgets('TC-02: 未选中 tab 的图标颜色应为 textColorPrimary', (tester) async { - await tester.pumpWidget(_buildTestApp(_buildIconTextTabBar(currentIndex: 0))); + await tester + .pumpWidget(_buildTestApp(_buildIconTextTabBar(currentIndex: 0))); await tester.pumpAndSettle(); final BuildContext context = tester.element(find.byType(TBottomTabBar)); final expectedColor = TTheme.of(context).textColorPrimary; - final iconThemes = tester.widgetList(find.byType(IconTheme)).toList(); + final iconThemes = + tester.widgetList(find.byType(IconTheme)).toList(); expect(iconThemes, isNotEmpty, reason: '未选中图标应被 IconTheme 包裹以注入颜色'); final unselectedIconTheme = iconThemes.firstWhere( @@ -150,24 +154,29 @@ void main() { await tester.tap(find.text('我的')); await tester.pumpAndSettle(); - final iconThemesAfter = tester.widgetList(find.byType(IconTheme)).toList(); + final iconThemesAfter = + tester.widgetList(find.byType(IconTheme)).toList(); final colors = iconThemesAfter.map((t) => t.data.color).toSet(); - expect(colors, contains(brandColor), reason: '切换后新选中 tab 图标应为 brandNormalColor'); - expect(colors, contains(primaryColor), reason: '切换后旧 tab 图标应为 textColorPrimary'); + expect(colors, contains(brandColor), + reason: '切换后新选中 tab 图标应为 brandNormalColor'); + expect(colors, contains(primaryColor), + reason: '切换后旧 tab 图标应为 textColorPrimary'); }); }); group('TBottomTabBar — icon 类型图标颜色 (issue #900 同类问题)', () { // TC-04: icon 类型选中 tab 图标颜色为 brandNormalColor - testWidgets('TC-04: icon 类型 — 选中 tab 图标颜色应为 brandNormalColor', (tester) async { + testWidgets('TC-04: icon 类型 — 选中 tab 图标颜色应为 brandNormalColor', + (tester) async { await tester.pumpWidget(_buildTestApp(_buildIconTabBar(currentIndex: 0))); await tester.pumpAndSettle(); final BuildContext context = tester.element(find.byType(TBottomTabBar)); final expectedColor = TTheme.of(context).brandNormalColor; - final iconThemes = tester.widgetList(find.byType(IconTheme)).toList(); + final iconThemes = + tester.widgetList(find.byType(IconTheme)).toList(); expect(iconThemes, isNotEmpty); final match = iconThemes.firstWhere( @@ -181,14 +190,16 @@ void main() { }); // TC-05: icon 类型未选中 tab 图标颜色为 textColorPrimary - testWidgets('TC-05: icon 类型 — 未选中 tab 图标颜色应为 textColorPrimary', (tester) async { + testWidgets('TC-05: icon 类型 — 未选中 tab 图标颜色应为 textColorPrimary', + (tester) async { await tester.pumpWidget(_buildTestApp(_buildIconTabBar(currentIndex: 0))); await tester.pumpAndSettle(); final BuildContext context = tester.element(find.byType(TBottomTabBar)); final expectedColor = TTheme.of(context).textColorPrimary; - final iconThemes = tester.widgetList(find.byType(IconTheme)).toList(); + final iconThemes = + tester.widgetList(find.byType(IconTheme)).toList(); final match = iconThemes.firstWhere( (t) => t.data.color == expectedColor, orElse: () => throw TestFailure( @@ -203,7 +214,8 @@ void main() { group('TBottomTabBar — 回归检查', () { // TC-06: 文字颜色不受影响 testWidgets('TC-06: iconText 类型文字颜色回归验证', (tester) async { - await tester.pumpWidget(_buildTestApp(_buildIconTextTabBar(currentIndex: 0))); + await tester + .pumpWidget(_buildTestApp(_buildIconTextTabBar(currentIndex: 0))); await tester.pumpAndSettle(); final BuildContext context = tester.element(find.byType(TBottomTabBar)); @@ -217,14 +229,16 @@ void main() { // 找到 textColor 为 brandColor 的文字(选中) final selectedText = tTexts.firstWhere( (t) => t.textColor == brandColor, - orElse: () => throw TestFailure('未找到 textColor == brandNormalColor 的 TText'), + orElse: () => + throw TestFailure('未找到 textColor == brandNormalColor 的 TText'), ); expect(selectedText.textColor, equals(brandColor)); // 找到 textColor 为 primaryColor 的文字(未选中) final unselectedText = tTexts.firstWhere( (t) => t.textColor == primaryColor, - orElse: () => throw TestFailure('未找到 textColor == textColorPrimary 的 TText'), + orElse: () => + throw TestFailure('未找到 textColor == textColorPrimary 的 TText'), ); expect(unselectedText.textColor, equals(primaryColor)); }); diff --git a/tdesign-component/test/t_picker_test.dart b/tdesign-component/test/t_picker_test.dart index d3da36c71..b5c57eb1d 100644 --- a/tdesign-component/test/t_picker_test.dart +++ b/tdesign-component/test/t_picker_test.dart @@ -399,8 +399,7 @@ void main() { expect(find.byType(ListWheelScrollView), findsNWidgets(3)); }); - testWidgets('初始值非首项 - onChange 返回正确值(多列独立)', - (WidgetTester tester) async { + testWidgets('初始值非首项 - onChange 返回正确值(多列独立)', (WidgetTester tester) async { TPickerValue? captured; const testData = [ [ @@ -438,8 +437,7 @@ void main() { expect(captured!.labels[1], 'B2'); }); - testWidgets('初始值非首项 - 联动模式 onChange 返回正确值', - (WidgetTester tester) async { + testWidgets('初始值非首项 - 联动模式 onChange 返回正确值', (WidgetTester tester) async { TPickerValue? captured; final linkedData = { const TPickerOption(label: '广东省', value: 'GD'): { @@ -478,8 +476,7 @@ void main() { expect(captured!.values[1], 'SZ'); }); - testWidgets('disabled 项修正 - 独立模式滚动不会 crash', - (WidgetTester tester) async { + testWidgets('disabled 项修正 - 独立模式滚动不会 crash', (WidgetTester tester) async { TPickerValue? captured; const testData = [ [ @@ -512,8 +509,7 @@ void main() { } }); - testWidgets('didUpdateWidget - items 变化触发重建', - (WidgetTester tester) async { + testWidgets('didUpdateWidget - items 变化触发重建', (WidgetTester tester) async { const testData1 = [ [TPickerOption(label: 'A', value: 'a')], ]; @@ -548,8 +544,7 @@ void main() { expect(find.byType(TPicker), findsOneWidget); }); - testWidgets('didUpdateWidget - items 变化触发重建', - (WidgetTester tester) async { + testWidgets('didUpdateWidget - items 变化触发重建', (WidgetTester tester) async { const testData = [ [ TPickerOption(label: 'A', value: 'a'), @@ -629,8 +624,7 @@ void main() { expect(find.byType(TPicker), findsOneWidget); }); - testWidgets('联动模式 - 列数变化后能恢复', - (WidgetTester tester) async { + testWidgets('联动模式 - 列数变化后能恢复', (WidgetTester tester) async { final linkedData = { const TPickerOption(label: '广东省', value: 'GD'): { const TPickerOption(label: '深圳市', value: 'SZ'): const [ @@ -679,8 +673,7 @@ void main() { expect(find.byType(ListWheelScrollView), findsNWidgets(3)); }); - testWidgets('联动模式 - 滚动后新列选中首项', - (WidgetTester tester) async { + testWidgets('联动模式 - 滚动后新列选中首项', (WidgetTester tester) async { TPickerValue? captured; final linkedData = { const TPickerOption(label: '广东省', value: 'GD'): { @@ -689,7 +682,8 @@ void main() { TPickerOption(label: '福田区', value: 'FT'), TPickerOption(label: '罗湖区', value: 'LL'), ], - const TPickerOption(label: '广州市', value: 'GZ'): const [], + const TPickerOption(label: '广州市', value: 'GZ'): + const [], }, }; @@ -723,8 +717,7 @@ void main() { expect(captured!.values[2], 'NS'); }); - testWidgets('onLoad 回调 - 滚动时触发', - (WidgetTester tester) async { + testWidgets('onLoad 回调 - 滚动时触发', (WidgetTester tester) async { var loadCallCount = 0; int? capturedColumn; int? capturedRemaining; @@ -765,8 +758,7 @@ void main() { expect(capturedRemaining, greaterThanOrEqualTo(0)); }); - testWidgets('onLoad 回调 - 联动模式 parentValue 正确', - (WidgetTester tester) async { + testWidgets('onLoad 回调 - 联动模式 parentValue 正确', (WidgetTester tester) async { dynamic capturedParentValue; int? capturedColumn; final linkedData = { @@ -915,7 +907,8 @@ void main() { [TPickerOption(label: 'A', value: 'a')], [TPickerOption(label: 'B', value: 'b')], ]; - final result = TPickerNormalize.normalizeColumns(input, TPickerKeys.defaults); + final result = + TPickerNormalize.normalizeColumns(input, TPickerKeys.defaults); expect(identical(result, input), true); }); @@ -923,7 +916,8 @@ void main() { final input = { const TPickerOption(label: 'A', value: 'a'): null, }; - final result = TPickerNormalize.normalizeLinked(input, TPickerKeys.defaults); + final result = + TPickerNormalize.normalizeLinked(input, TPickerKeys.defaults); expect(identical(result, input), true); }); @@ -937,7 +931,8 @@ void main() { {'label': 'C', 'value': 'c'}, ], ]; - final result = TPickerNormalize.normalizeColumns(input, TPickerKeys.defaults); + final result = + TPickerNormalize.normalizeColumns(input, TPickerKeys.defaults); expect(result, isA>>()); expect(result.length, 2); expect(result[0].length, 2); @@ -952,7 +947,8 @@ void main() { ], }, }; - final result = TPickerNormalize.normalizeLinked(input, const TPickerKeys(label: 'label', value: 'value')); + final result = TPickerNormalize.normalizeLinked( + input, const TPickerKeys(label: 'label', value: 'value')); expect(result, isA>()); }); @@ -966,7 +962,8 @@ void main() { ], }, }; - final result = TPickerNormalize.normalizeLinked(input, TPickerKeys.defaults); + final result = + TPickerNormalize.normalizeLinked(input, TPickerKeys.defaults); expect(result, isA>()); final keyA = result.keys.first; @@ -1009,8 +1006,7 @@ void main() { expect(find.byType(TPicker), findsOneWidget); }); - testWidgets('disabled 项修正 - 正向查找最近 enabled', - (WidgetTester tester) async { + testWidgets('disabled 项修正 - 正向查找最近 enabled', (WidgetTester tester) async { TPickerValue? captured; const testData = [ [ @@ -1042,8 +1038,7 @@ void main() { } }); - testWidgets('disabled 项修正 - 反向查找最近 enabled', - (WidgetTester tester) async { + testWidgets('disabled 项修正 - 反向查找最近 enabled', (WidgetTester tester) async { TPickerValue? captured; const testData = [ [ @@ -1080,7 +1075,8 @@ void main() { ['北京', '上海', '广州'], ['朝阳区', '浦东'], ]; - final result = TPickerNormalize.normalizeColumns(input, TPickerKeys.defaults); + final result = + TPickerNormalize.normalizeColumns(input, TPickerKeys.defaults); expect(result, isA>>()); expect(result[0][0].label, '北京'); expect(result[0][0].value, '北京'); // 纯字符串时 value == label @@ -1088,21 +1084,26 @@ void main() { }); test('TPickerNormalize - 空列表归一化', () { - final result = TPickerNormalize.normalizeColumns([], TPickerKeys.defaults); + final result = + TPickerNormalize.normalizeColumns([], TPickerKeys.defaults); expect(result, isA>>()); expect(result.isEmpty, true); }); test('TPickerNormalize - 空 Map 归一化', () { - final result = - TPickerNormalize.normalizeLinked({}, TPickerKeys.defaults); + final result = TPickerNormalize.normalizeLinked( + {}, TPickerKeys.defaults); expect(result, isA>()); expect(result.isEmpty, true); }); test('TPickerNormalize - List 中包含非 List 元素得到空列', () { - final input = ['not_a_list', ['A', 'B']]; - final result = TPickerNormalize.normalizeColumns(input, TPickerKeys.defaults); + final input = [ + 'not_a_list', + ['A', 'B'] + ]; + final result = + TPickerNormalize.normalizeColumns(input, TPickerKeys.defaults); expect(result, isA>>()); expect(result[0], isEmpty); // 非 List 元素归一化为空列 expect(result[1].length, 2); @@ -1115,7 +1116,8 @@ void main() { {'name': '广州', 'code': 'GZ'}, ], ]; - const keys = TPickerKeys(label: 'name', value: 'code', disabled: 'readonly'); + const keys = + TPickerKeys(label: 'name', value: 'code', disabled: 'readonly'); final result = TPickerNormalize.normalizeColumns(input, keys); expect(result[0][0].label, '深圳'); expect(result[0][0].value, 'SZ'); @@ -1127,7 +1129,8 @@ void main() { final input = { 'A': ['X', 'Y'], }; - final result = TPickerNormalize.normalizeLinked(input, TPickerKeys.defaults); + final result = + TPickerNormalize.normalizeLinked(input, TPickerKeys.defaults); expect(result, isA>()); final keyA = result.keys.first; expect(keyA.label, 'A'); @@ -1141,7 +1144,8 @@ void main() { final input = { 'A': 12345, // 既不是 Map 也不是 List }; - final result = TPickerNormalize.normalizeLinked(input, TPickerKeys.defaults); + final result = + TPickerNormalize.normalizeLinked(input, TPickerKeys.defaults); final child = result[result.keys.first]; expect(child, isA>()); expect((child as List).isEmpty, true); @@ -1154,7 +1158,8 @@ void main() { {'label': 'Y', 'value': 'y'}, ], ]; - final result = TPickerNormalize.normalizeColumns(input, TPickerKeys.defaults); + final result = + TPickerNormalize.normalizeColumns(input, TPickerKeys.defaults); expect(result[0][0].label, 'X'); expect(result[0][1].label, 'Y'); }); @@ -1163,7 +1168,8 @@ void main() { final input = { null: ['child1'], }; - final result = TPickerNormalize.normalizeLinked(input, TPickerKeys.defaults); + final result = + TPickerNormalize.normalizeLinked(input, TPickerKeys.defaults); final key = result.keys.first; expect(key.label, ''); expect(key.value, null); @@ -1250,14 +1256,18 @@ void main() { expect(columns.columns[0][0].label, '北京'); final linked = TPickerLinked.fromRaw(const { - '广东': {'深圳': ['南山']}, + '广东': { + '深圳': ['南山'] + }, }); expect(linked.tree.keys.first.label, '广东'); }); test('TPickerKeys - 值相同的不同实例 hashCode 一致', () { - const a = TPickerKeys(label: 'x', value: 'y', disabled: 'd', children: 'c'); - const b = TPickerKeys(label: 'x', value: 'y', disabled: 'd', children: 'c'); + const a = + TPickerKeys(label: 'x', value: 'y', disabled: 'd', children: 'c'); + const b = + TPickerKeys(label: 'x', value: 'y', disabled: 'd', children: 'c'); expect(a == b, true); expect(a.hashCode, b.hashCode); }); @@ -1292,7 +1302,8 @@ void main() { Text('selected: $selected'), TPicker( items: const TPickerColumns(testData), - onChange: (v) => setState(() => selected = v.values.first as String), + onChange: (v) => + setState(() => selected = v.values.first as String), ), ], ); @@ -1349,7 +1360,8 @@ void main() { TPicker( items: TPickerLinked(linkedData), initialValue: const ['GD', 'SZ', 'NS'], - onChange: (v) => setState(() => selected = v.labels.join(' / ')), + onChange: (v) => + setState(() => selected = v.labels.join(' / ')), ), ], ); @@ -1413,8 +1425,7 @@ void main() { expect(a == c, false); }); - testWidgets('工具栏 - 默认显示取消/确认按钮', - (WidgetTester tester) async { + testWidgets('工具栏 - 默认显示取消/确认按钮', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1435,8 +1446,7 @@ void main() { expect(find.text('确认'), findsOneWidget); }); - testWidgets('工具栏 - 设置 title 后中部显示标题', - (WidgetTester tester) async { + testWidgets('工具栏 - 设置 title 后中部显示标题', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1459,8 +1469,7 @@ void main() { expect(find.text('请选择'), findsOneWidget); }); - testWidgets('工具栏 - 点击取消触发 onCancel', - (WidgetTester tester) async { + testWidgets('工具栏 - 点击取消触发 onCancel', (WidgetTester tester) async { var cancelled = false; await tester.pumpWidget( MaterialApp( @@ -1481,8 +1490,7 @@ void main() { expect(cancelled, true); }); - testWidgets('工具栏 - 点击确认触发 onConfirm 并携带选中值', - (WidgetTester tester) async { + testWidgets('工具栏 - 点击确认触发 onConfirm 并携带选中值', (WidgetTester tester) async { TPickerValue? confirmedValue; await tester.pumpWidget( MaterialApp( @@ -1533,8 +1541,7 @@ void main() { expect(find.text('确认'), findsNothing); }); - testWidgets('工具栏 - cancel 自定义插槽(图标)', - (WidgetTester tester) async { + testWidgets('工具栏 - cancel 自定义插槽(图标)', (WidgetTester tester) async { var cancelled = false; await tester.pumpWidget( MaterialApp( @@ -1558,8 +1565,7 @@ void main() { expect(cancelled, true); }); - testWidgets('工具栏 - confirm 自定义插槽(图标)', - (WidgetTester tester) async { + testWidgets('工具栏 - confirm 自定义插槽(图标)', (WidgetTester tester) async { TPickerValue? confirmedValue; await tester.pumpWidget( MaterialApp( @@ -1590,8 +1596,7 @@ void main() { expect(confirmedValue!.labels.first, 'Y'); }); - testWidgets('工具栏 - titleWidget 自定义标题插槽', - (WidgetTester tester) async { + testWidgets('工具栏 - titleWidget 自定义标题插槽', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( diff --git a/tdesign-component/test/t_popup_coverage_test.dart b/tdesign-component/test/t_popup_coverage_test.dart new file mode 100644 index 000000000..295d6cb84 --- /dev/null +++ b/tdesign-component/test/t_popup_coverage_test.dart @@ -0,0 +1,1238 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tdesign_flutter/src/components/popup/t_popup.dart' + show PopupHeader, PopupLayout; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +import 'helpers/popup_test_helpers.dart'; +import 'helpers/popup_test_resource.dart'; + +void main() { + tearDown(resetPopupTestResource); + + group('TPopup 覆盖率补充', () { + testWidgets('bottom 仅标题行(无操作栏)', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + titleWidget: TText('仅标题行'), + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('仅标题行'), findsOneWidget); + expect(find.text('取消'), findsNothing); + expect(find.text('确定'), findsNothing); + }); + + testWidgets('bottom 仅标题时可正常渲染标题内容', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + titleWidget: TText('左对齐标题'), + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('左对齐标题'), findsOneWidget); + }); + + testWidgets('bottom 仅隐藏 confirm 槽位', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + confirmBuilder: null, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('取消'), findsOneWidget); + expect(find.text('确定'), findsNothing); + }); + + testWidgets('bottom 仅隐藏 cancel 槽位', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + cancelBuilder: null, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('取消'), findsNothing); + expect(find.text('确定'), findsOneWidget); + }); + + testWidgets('center closeBuilder 自定义关闭区', (tester) async { + late BuildContext hostContext; + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.center, + width: 160, + height: 120, + closeBuilder: (_, close) => TextButton( + onPressed: close, + child: const Text('builder关闭'), + ), + child: const SizedBox(height: 80, width: 120)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('builder关闭'), findsOneWidget); + await tester.tap(find.text('builder关闭')); + await tester.pumpAndSettle(); + }); + + testWidgets('center 默认关闭按钮点击关闭浮层', (tester) async { + late BuildContext hostContext; + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.center, + width: 120, + height: 120, + child: const SizedBox(height: 80, width: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.byIcon(TIcons.close_circle), findsOneWidget); + await tester.tap(find.byIcon(TIcons.close_circle)); + await tester.pumpAndSettle(); + expect(find.byIcon(TIcons.close_circle), findsNothing); + }); + + testWidgets('bottom 无固定 height 贴底布局', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 80, width: 200)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.byType(Positioned), findsWidgets); + }); + + testWidgets('top 无固定 height 可打开', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.top, + child: const SizedBox(height: 60, width: 200)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.byType(Positioned), findsWidgets); + }); + + testWidgets('center closeBuilder=null 无下方关闭', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.center, + width: 100, + height: 100, + closeBuilder: null, + child: const SizedBox(height: 80, width: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.byIcon(TIcons.close_circle), findsNothing); + expect(find.byType(Center), findsWidgets); + }); + + testWidgets('handle 重复 close 安全', (tester) async { + TPopupHandle? handle; + await openPopup( + tester, + onPressed: () { + handle = TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 80, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 40)), + ); + }, + ); + await tester.pumpAndSettle(); + handle!.close(); + handle!.close(); + await tester.pumpAndSettle(); + expect(handle!.isShowing, isFalse); + }); + + testWidgets('handle.open 关闭后再次打开触发 onOpen / onOpened', (tester) async { + var openCount = 0; + var openedCount = 0; + late BuildContext hostContext; + TPopupHandle? handle; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + handle = TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 80, + cancelBuilder: null, + confirmBuilder: null, + onOpen: () => openCount++, + onOpened: () => openedCount++, + child: const SizedBox(height: 40)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(openCount, 1); + expect(openedCount, 1); + + handle!.close(); + await tester.pumpAndSettle(); + + handle!.open(hostContext); + await tester.pumpAndSettle(); + expect(openCount, 2); + expect(openedCount, 2); + }); + + testWidgets('TToolbarPressable 按压与禁用', (tester) async { + var tapped = false; + await tester.pumpWidget( + MaterialApp( + home: TTheme( + data: TThemeData.defaultData(), + child: Scaffold( + body: TToolbarPressable( + onTap: () => tapped = true, + child: const Text('press'), + ), + ), + ), + ), + ); + await tester.tap(find.text('press')); + await tester.pumpAndSettle(); + expect(tapped, isTrue); + + await tester.pumpWidget( + MaterialApp( + home: TTheme( + data: TThemeData.defaultData(), + child: Scaffold( + body: TToolbarPressable( + onTap: null, + mergeTextStyle: const TextStyle(color: Colors.red), + mergeIconTheme: const IconThemeData(color: Colors.red), + child: const Icon(Icons.add), + ), + ), + ), + ), + ); + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + }); + }); + + group('TPopupOptions 覆盖率补充', () { + test('hasBuiltInHeader 识别 titleWidget', () { + expect( + TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.bottom, + titleWidget: const Text('w'), + cancelBuilder: null, + confirmBuilder: null, + ).hasBuiltInHeader, + isTrue, + ); + }); + + test('useCustomHeader 与 useDefaultHeader 互斥', () { + final custom = TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.bottom, + headerBuilder: (_, __) => const Text('h'), + ); + expect(custom.useCustomHeader, isTrue); + expect(custom.useDefaultHeader, isFalse); + + final titleOnly = TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.bottom, + titleWidget: TText('仅标题'), + cancelBuilder: null, + confirmBuilder: null, + ); + expect(titleOnly.useDefaultHeader, isTrue); + expect(titleOnly.useCustomHeader, isFalse); + expect(titleOnly.hasBuiltInHeader, isTrue); + }); + + test('assertPlacementParams 覆盖各 placement 的字段提示', () { + // bottom + width → 抛 FlutterError + expect( + () => TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.bottom, + width: 200, + ).assertPlacementParams(), + throwsA(isA()), + ); + // top 默认配置 → 不抛 + expect( + () => TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.top, + ).assertPlacementParams(), + returnsNormally, + ); + // center 合法字段 → 不抛 + expect( + () => TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.center, + height: 100, + closeBuilder: null, + ).assertPlacementParams(), + returnsNormally, + ); + expect( + () => TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.center, + ).assertPlacementParams(), + returnsNormally, + ); + }); + }); + + group('TPopup 覆盖率深化', () { + testWidgets('headerBuilder 自定义内容可正常渲染', + (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + headerBuilder: (ctx, close) => Column( + children: const [ + Text('头Widget'), + Row( + children: [ + Text('builder左'), + Text('builder右'), + ], + ), + ], + ), + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('头Widget'), findsOneWidget); + expect(find.text('builder左'), findsOneWidget); + expect(find.text('builder右'), findsOneWidget); + }); + + testWidgets('headerBuilder 自定义行内内容可正常渲染', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + headerBuilder: (ctx, close) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + Text('左槽Widget'), + Text('右槽Widget'), + ], + ), + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('左槽Widget'), findsOneWidget); + expect(find.text('右槽Widget'), findsOneWidget); + }); + + testWidgets('操作栏使用自定义 cancel/confirm Widget(非 Builder)', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + cancelBuilder: (_, __) => const Text('自定义左'), + confirmBuilder: (_, __) => const Text('自定义右'), + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('自定义左'), findsOneWidget); + expect(find.text('自定义右'), findsOneWidget); + }); + + testWidgets('onVisibleChange 记录各关闭触发源', (tester) async { + final hideTriggers = []; + late BuildContext hostContext; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + titleWidget: TText('标题'), + onVisibleChange: (visible, trigger) { + if (!visible) { + hideTriggers.add(trigger); + } + }, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('取消')); + await tester.pumpAndSettle(); + expect(hideTriggers.last, TPopupTrigger.cancel); + + await openPopup( + tester, + onPressed: () { + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + onVisibleChange: (visible, trigger) { + if (!visible) { + hideTriggers.add(trigger); + } + }, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tapAt(const Offset(10, 10)); + await tester.pumpAndSettle(); + expect(hideTriggers.last, TPopupTrigger.overlay); + + await openPopup( + tester, + onPressed: () { + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.center, + width: 120, + height: 120, + onVisibleChange: (visible, trigger) { + if (!visible) { + hideTriggers.add(trigger); + } + }, + child: const SizedBox(height: 80, width: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(TIcons.close_circle)); + await tester.pumpAndSettle(); + expect(hideTriggers.last, TPopupTrigger.close); + }); + + testWidgets('Popup 内嵌套 show 可再开一层且先关内层', (tester) async { + TPopupHandle? outerHandle; + TPopupHandle? innerHandle; + + await openPopup( + tester, + onPressed: () { + outerHandle = TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 200, + cancelBuilder: null, + confirmBuilder: null, + child: Builder( + builder: (ctx) { + return ElevatedButton( + onPressed: () { + innerHandle = TPopup.show( + ctx, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 120, + cancelBuilder: null, + confirmBuilder: null, + child: const Text('内层'), + ), + ); + }, + child: const Text('开内层'), + ); + }, + )), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('开内层'), findsOneWidget); + + await tester.tap(find.text('开内层')); + await tester.pumpAndSettle(); + expect(find.text('内层'), findsOneWidget); + + innerHandle!.close(); + await tester.pumpAndSettle(); + expect(find.text('内层'), findsNothing); + expect(find.text('开内层'), findsOneWidget); + + outerHandle!.close(); + await tester.pumpAndSettle(); + }); + + testWidgets('modal 为 false 仍可打开关闭', (tester) async { + TPopupHandle? handle; + await openPopup( + tester, + onPressed: () { + handle = TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 120, + showOverlay: false, + modal: false, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + handle!.close(); + await tester.pumpAndSettle(); + }); + + testWidgets('center 自定义 radius 与 backgroundColor', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.center, + width: 100, + height: 100, + radius: 4, + backgroundColor: Colors.red, + child: const SizedBox(height: 60, width: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + final hasRedPanel = + tester.widgetList(find.byType(Container)).any((c) { + final d = c.decoration; + return d is BoxDecoration && d.color == Colors.red; + }); + expect(hasRedPanel, isTrue); + }); + + testWidgets('bottom inset.left/right 与无 overlay 仍可关闭', (tester) async { + TPopupHandle? handle; + await openPopup( + tester, + onPressed: () { + handle = TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 100, + inset: const TPopupBottomInset(left: 16, right: 16), + showOverlay: false, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + handle!.close(); + await tester.pumpAndSettle(); + }); + }); + + group('PopupLayout 覆盖率补充', () { + testWidgets('bottom 无 height 时贴底', (tester) async { + final layout = PopupLayout( + placement: TPopupPlacement.bottom, + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Stack( + children: [ + layout.wrapPositioned(child: const SizedBox(height: 1)) + ], + ), + ), + ), + ); + final positioned = tester.widget(find.byType(Positioned)); + expect(positioned.bottom, 0); + expect(positioned.top, isNull); + expect(positioned.height, isNull); + }); + + test('alignment center', () { + expect( + PopupLayout( + placement: TPopupPlacement.center, + ).alignment, + Alignment.center, + ); + }); + + testWidgets('right 默认 drawer 宽度与 inset', (tester) async { + final layout = PopupLayout( + placement: TPopupPlacement.right, + inset: const TPopupRightInset(top: 8, bottom: 8), + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Stack( + children: [layout.wrapPositioned(child: const SizedBox())], + ), + ), + ), + ); + final positioned = tester.widget(find.byType(Positioned)); + expect(positioned.width, PopupLayout.defaultDrawerWidth); + expect(positioned.right, 0); + expect(positioned.top, 8); + }); + + testWidgets('left 默认 drawer 宽度', (tester) async { + final layout = PopupLayout( + placement: TPopupPlacement.left, + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Stack( + children: [layout.wrapPositioned(child: const SizedBox())], + ), + ), + ), + ); + expect( + tester.widget(find.byType(Positioned)).width, + PopupLayout.defaultDrawerWidth, + ); + }); + }); + + // ============================================================ + // TPopupOptions 命名工厂(B 方案):编译期挡误用 + 运行期与默认构造等价 + // ============================================================ + group('TPopupOptions 命名工厂', () { + test('.bottom 生成的 options 等价于默认构造(含默认 sentinel)', () { + final factoryOpts = TPopupOptions.bottom( + child: const SizedBox(), + height: 300, + ); + final baseOpts = TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.bottom, + height: 300, + ); + expect(factoryOpts.placement, baseOpts.placement); + expect(factoryOpts.height, 300); + expect(factoryOpts.width, isNull); + expect(factoryOpts.usesDefaultHeader, isTrue); + expect(factoryOpts.usesDefaultCancel, isTrue); + expect(factoryOpts.usesDefaultConfirm, isTrue); + }); + + test('.center 生成 placement=center + 默认 closeBuilder', () { + final opts = TPopupOptions.center( + child: const SizedBox(), + width: 220, + height: 220, + ); + expect(opts.placement, TPopupPlacement.center); + expect(opts.width, 220); + expect(opts.height, 220); + expect(opts.usesDefaultClose, isTrue); + expect(opts.inset, isNull); + }); + + test('.top / .left / .right 生成对应 placement + 默认无头部', () { + final top = TPopupOptions.top(child: const SizedBox(), height: 100); + expect(top.placement, TPopupPlacement.top); + expect(top.height, 100); + expect(top.width, isNull); + + final left = TPopupOptions.left(child: const SizedBox(), width: 280); + expect(left.placement, TPopupPlacement.left); + expect(left.width, 280); + expect(left.height, isNull); + + final right = TPopupOptions.right(child: const SizedBox(), width: 280); + expect(right.placement, TPopupPlacement.right); + expect(right.width, 280); + expect(right.height, isNull); + }); + + test('factory 输出的合法配置在 assertPlacementParams 下零异常', () { + final variants = [ + TPopupOptions.bottom(child: const SizedBox(), height: 300), + TPopupOptions.center(child: const SizedBox(), width: 220, height: 220), + TPopupOptions.top(child: const SizedBox(), height: 100), + TPopupOptions.left(child: const SizedBox(), width: 280), + TPopupOptions.right(child: const SizedBox(), width: 280), + ]; + for (final opts in variants) { + expect(opts.assertPlacementParams, returnsNormally, + reason: 'placement=${opts.placement}'); + } + }); + + test('factory 透传通用字段:animationDuration / overlay / callbacks', () { + var visibleChanges = 0; + final opts = TPopupOptions.bottom( + child: const SizedBox(), + animationDuration: const Duration(milliseconds: 500), + showOverlay: false, + overlayOpacity: 0.5, + destroyOnClose: true, + onVisibleChange: (_, __) => visibleChanges++, + ); + expect(opts.animationDuration, const Duration(milliseconds: 500)); + expect(opts.showOverlay, isFalse); + expect(opts.closeOnOverlayClick, isFalse); + expect(opts.overlayOpacity, 0.5); + expect(opts.destroyOnClose, isTrue); + opts.onVisibleChange?.call(false, TPopupTrigger.api); + expect(visibleChanges, 1); + }); + }); + + // ============================================================ + // TPopupOptions.copyWith:处理 sentinel 三态 + // ============================================================ + group('TPopupOptions.copyWith', () { + test('不传字段 → 完全继承原值(含 sentinel 默认 builder)', () { + final base = TPopupOptions(child: const SizedBox()); + final next = base.copyWith(); + expect(next.placement, base.placement); + expect(next.height, base.height); + // sentinel 身份延续 + expect(next.usesDefaultHeader, isTrue); + expect(next.usesDefaultCancel, isTrue); + expect(next.usesDefaultConfirm, isTrue); + expect(next.usesDefaultClose, isTrue); + }); + + test('显式传 null → 把对应 builder 置为「隐藏」', () { + final base = TPopupOptions(child: const SizedBox()); + final next = base.copyWith(headerBuilder: null, cancelBuilder: null); + expect(next.headerBuilder, isNull); + expect(next.cancelBuilder, isNull); + // 未传的字段仍是 sentinel + expect(next.usesDefaultConfirm, isTrue); + expect(next.usesDefaultClose, isTrue); + }); + + test('传自定义 builder → 替换为自定义', () { + final base = TPopupOptions(child: const SizedBox()); + const titleWidget = Text('t'); + final next = base.copyWith(titleWidget: titleWidget); + expect(next.titleWidget, same(titleWidget)); + // 其它继承 + expect(next.usesDefaultHeader, isTrue); + }); + + test('值类字段(width / height / radius / 颜色 / Color?)三态正确', () { + final base = TPopupOptions( + child: const SizedBox(), + width: 100, + height: 200, + radius: 8, + backgroundColor: const Color(0xFF000000), + ); + // 不传:继承 + expect(base.copyWith().width, 100); + // 显式 null:清空 + expect(base.copyWith(width: null).width, isNull); + expect(base.copyWith(backgroundColor: null).backgroundColor, isNull); + // 替换 + expect(base.copyWith(width: 50).width, 50); + }); + + test('回调字段同样支持三态', () { + hookA(bool _, TPopupTrigger __) {} + final base = TPopupOptions( + child: const SizedBox(), + onVisibleChange: hookA, + ); + expect(base.copyWith().onVisibleChange, same(hookA)); + expect(base.copyWith(onVisibleChange: null).onVisibleChange, isNull); + }); + }); + + // ============================================================ + // TPopupHandle.open(): 缓存 navigator,无 context 再开 + // ============================================================ + group('TPopupHandle.open() 无参复用缓存 navigator', () { + testWidgets('close 后 open() 不传 context 仍可重新打开', (tester) async { + late TPopupHandle handle; + await openPopup( + tester, + onPressed: () { + handle = TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + titleWidget: const Text('再开测试'), + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(handle.isShowing, isTrue); + + handle.close(); + await tester.pumpAndSettle(); + expect(handle.isShowing, isFalse); + + // 关键:不传 context,使用首次缓存的 navigator + handle.open(); + await tester.pumpAndSettle(); + expect(handle.isShowing, isTrue); + expect(find.text('再开测试'), findsOneWidget); + }); + + testWidgets('已展示状态下 open() 重复调用无副作用', (tester) async { + late TPopupHandle handle; + await openPopup( + tester, + onPressed: () { + handle = TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + handle.open(); // 无参数重复 + await tester.pumpAndSettle(); + expect(handle.isShowing, isTrue); + // 只有一个 popup route + expect(find.byType(SizedBox), findsWidgets); + }); + }); + + // ============================================================ + // Header 三态行为 + 触发源精细化(覆盖 _popup_header.dart 短路 fix) + // ============================================================ + group('Popup Header 三态完整对照', () { + /// 拿到 popup 内 PopupHeader 实际占用的高度。 + /// - 无头部(headerBuilder=null 或三槽全 null):== 0 + /// - 默认 sentinel header:== PopupHeader.headerHeight(58) + /// - 自定义 headerBuilder:取决于自定义 widget 的高度 + double popupHeaderHeight(WidgetTester tester) { + return tester.getSize(find.byType(PopupHeader)).height; + } + + testWidgets('headerBuilder=null 同时传 title/cancel/confirmBuilder 也不渲染', + (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 180, + headerBuilder: null, // 一票否决 + titleWidget: const Text('应隐藏-标题'), + cancelBuilder: (_, close) => + GestureDetector(onTap: close, child: const Text('应隐藏-左')), + confirmBuilder: (_, close) => + GestureDetector(onTap: close, child: const Text('应隐藏-右')), + child: + const SizedBox(key: ValueKey('popup-child'), height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('应隐藏-标题'), findsNothing); + expect(find.text('应隐藏-左'), findsNothing); + expect(find.text('应隐藏-右'), findsNothing); + // 也不应该有内置文案 + expect(find.text('取消'), findsNothing); + expect(find.text('确定'), findsNothing); + }); + + testWidgets('headerBuilder=null → PopupHeader 高度精确为 0', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 180, + headerBuilder: null, + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(popupHeaderHeight(tester), 0); + }); + + testWidgets( + '默认 sentinel + 三槽全 null → PopupHeader 高度精确为 0(_popup_header.dart 修复回归)', + (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 180, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(popupHeaderHeight(tester), 0); + expect(find.text('取消'), findsNothing); + expect(find.text('确定'), findsNothing); + }); + + testWidgets('对照:默认 sentinel header(含标题)PopupHeader 高度 == 58', + (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 180, + titleWidget: const Text('标题'), + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(popupHeaderHeight(tester), PopupHeader.headerHeight); + }); + + testWidgets('仅 cancelBuilder=null → 隐藏左槽,标题与右槽仍渲染', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 180, + cancelBuilder: null, + titleWidget: const Text('标题在'), + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('标题在'), findsOneWidget); + expect(find.text('取消'), findsNothing); + expect(find.text('确定'), findsOneWidget); + }); + + testWidgets('仅 confirmBuilder=null → 隐藏右槽,标题与左槽仍渲染', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 180, + confirmBuilder: null, + titleWidget: const Text('标题在'), + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('标题在'), findsOneWidget); + expect(find.text('取消'), findsOneWidget); + expect(find.text('确定'), findsNothing); + }); + + testWidgets( + 'headerBuilder 自定义 → titleWidget / cancelBuilder / confirmBuilder 全部被忽略', + (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 180, + headerBuilder: (_, __) => const Text('唯一头部'), + titleWidget: const Text('不该出现-标题'), + cancelBuilder: (_, __) => const Text('不该出现-左'), + confirmBuilder: (_, __) => const Text('不该出现-右'), + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('唯一头部'), findsOneWidget); + expect(find.text('不该出现-标题'), findsNothing); + expect(find.text('不该出现-左'), findsNothing); + expect(find.text('不该出现-右'), findsNothing); + // 也找不到内置默认文案 + expect(find.text('取消'), findsNothing); + expect(find.text('确定'), findsNothing); + }); + }); + + // ============================================================ + // 触发源精细化:sentinel vs 自定义 builder 的 close 上报区分 + // ============================================================ + group('Popup 触发源细分(sentinel vs 自定义 builder)', () { + testWidgets('内置 cancel 按钮点击 → cancel(与 confirm 区分)', (tester) async { + TPopupTrigger? lastTrigger; + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + onVisibleChange: (visible, trigger) { + if (!visible) { + lastTrigger = trigger; + } + }, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('取消')); + await tester.pumpAndSettle(); + expect(lastTrigger, TPopupTrigger.cancel); + }); + + testWidgets('自定义 cancelBuilder 内调 close → cancel', (tester) async { + TPopupTrigger? lastTrigger; + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + cancelBuilder: (_, close) => GestureDetector( + onTap: close, + child: const Text('我自己的取消'), + ), + onVisibleChange: (visible, trigger) { + if (!visible) { + lastTrigger = trigger; + } + }, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('我自己的取消')); + await tester.pumpAndSettle(); + expect(lastTrigger, TPopupTrigger.cancel); + }); + + testWidgets('自定义 confirmBuilder 内调 close → confirm', (tester) async { + TPopupTrigger? lastTrigger; + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + confirmBuilder: (_, close) => GestureDetector( + onTap: close, + child: const Text('我自己的确定'), + ), + onVisibleChange: (visible, trigger) { + if (!visible) { + lastTrigger = trigger; + } + }, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('我自己的确定')); + await tester.pumpAndSettle(); + expect(lastTrigger, TPopupTrigger.confirm); + }); + + testWidgets('center 自定义 closeBuilder 内调 close → close', (tester) async { + TPopupTrigger? lastTrigger; + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.center, + width: 200, + height: 200, + closeBuilder: (_, close) => GestureDetector( + onTap: close, + child: const Text('我自己的关闭'), + ), + onVisibleChange: (visible, trigger) { + if (!visible) { + lastTrigger = trigger; + } + }, + child: const SizedBox(height: 80, width: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('我自己的关闭')); + await tester.pumpAndSettle(); + expect(lastTrigger, TPopupTrigger.close); + }); + + testWidgets('headerBuilder 自定义内调 close → custom(无预设动作语义)', (tester) async { + TPopupTrigger? lastTrigger; + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + headerBuilder: (_, close) => GestureDetector( + onTap: close, + child: const Text('整行自定义头部'), + ), + onVisibleChange: (visible, trigger) { + if (!visible) { + lastTrigger = trigger; + } + }, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('整行自定义头部')); + await tester.pumpAndSettle(); + expect(lastTrigger, TPopupTrigger.custom); + }); + }); +} diff --git a/tdesign-component/test/t_popup_layout_test.dart b/tdesign-component/test/t_popup_layout_test.dart new file mode 100644 index 000000000..915520b70 --- /dev/null +++ b/tdesign-component/test/t_popup_layout_test.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tdesign_flutter/src/components/popup/t_popup.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +void main() { + group('PopupLayout', () { + testWidgets('top placement 使用 height 与左右 inset', (tester) async { + final layout = PopupLayout( + placement: TPopupPlacement.top, + inset: const TPopupTopInset(left: 4, right: 6), + height: 100, + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Stack(children: [ + layout.wrapPositioned(child: const SizedBox(height: 50)) + ]), + ), + ), + ); + final positioned = tester.widget(find.byType(Positioned)); + expect(positioned.top, 0); + expect(positioned.left, 4); + expect(positioned.right, 6); + expect(positioned.height, 100); + }); + + testWidgets('bottom placement 使用左右 inset 与固定 height', (tester) async { + final layout = PopupLayout( + placement: TPopupPlacement.bottom, + inset: const TPopupBottomInset(left: 12, right: 20), + height: 200, + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Stack(children: [ + layout.wrapPositioned(child: const SizedBox(height: 50)) + ]), + ), + ), + ); + final positioned = tester.widget(find.byType(Positioned)); + expect(positioned.left, 12); + expect(positioned.right, 20); + expect(positioned.bottom, 0); + expect(positioned.height, 200); + }); + + testWidgets('bottom 无 height 时贴底', (tester) async { + final layout = PopupLayout( + placement: TPopupPlacement.bottom, + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Stack( + children: [ + layout.wrapPositioned(child: const SizedBox(height: 1)) + ], + ), + ), + ), + ); + final positioned = tester.widget(find.byType(Positioned)); + expect(positioned.bottom, 0); + expect(positioned.top, isNull); + expect(positioned.height, isNull); + }); + + testWidgets('left / right 使用默认或自定义 width 与上下 inset', (tester) async { + final left = PopupLayout( + placement: TPopupPlacement.left, + inset: const TPopupLeftInset(top: 56, bottom: 12), + width: 300, + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: + Stack(children: [left.wrapPositioned(child: const SizedBox())]), + ), + ), + ); + var positioned = tester.widget(find.byType(Positioned)); + expect(positioned.width, 300); + expect(positioned.top, 56); + expect(positioned.bottom, 12); + + final right = PopupLayout( + placement: TPopupPlacement.right, + inset: const TPopupRightInset(top: 8, bottom: 10), + width: 260, + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Stack( + children: [right.wrapPositioned(child: const SizedBox())], + ), + ), + ), + ); + positioned = tester.widget(find.byType(Positioned)); + expect(positioned.width, 260); + expect(positioned.top, 8); + expect(positioned.bottom, 10); + }); + + testWidgets('center placement 仅 Center 包裹(尺寸由 PopupShell 控制)', + (tester) async { + final layout = PopupLayout( + placement: TPopupPlacement.center, + width: 200, + height: 150, + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Stack( + children: [ + layout.wrapPositioned( + child: const SizedBox( + key: ValueKey('content'), + width: 200, + height: 150, + ), + ), + ], + ), + ), + ), + ); + expect(find.byType(Center), findsOneWidget); + final box = + tester.widget(find.byKey(const ValueKey('content'))); + expect(box.width, 200); + expect(box.height, 150); + }); + + test('slideOffset 五向偏移', () { + final layout = PopupLayout( + placement: TPopupPlacement.top, + ); + expect(layout.slideOffset(0), const Offset(0, -1)); + expect(layout.slideOffset(1), const Offset(0, 0)); + + final bottom = PopupLayout( + placement: TPopupPlacement.bottom, + ); + expect(bottom.slideOffset(0), const Offset(0, 1)); + + final left = PopupLayout( + placement: TPopupPlacement.left, + ); + expect(left.slideOffset(0.5), const Offset(-0.5, 0)); + + final right = PopupLayout( + placement: TPopupPlacement.right, + ); + expect(right.slideOffset(0.5), const Offset(0.5, 0)); + + final center = PopupLayout( + placement: TPopupPlacement.center, + ); + expect(center.slideOffset(0.5), Offset.zero); + }); + + testWidgets('center 仅 Positioned.fill + Center,由 PopupShell 控制尺寸', + (tester) async { + final layout = PopupLayout( + placement: TPopupPlacement.center, + width: 100, + height: 80, + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Stack( + children: [ + layout.wrapPositioned(child: const SizedBox(height: 200)), + ], + ), + ), + ), + ); + final center = tester.widget
(find.byType(Center)); + expect(center.child, isA()); + }); + + test('alignment 各方向', () { + expect( + PopupLayout( + placement: TPopupPlacement.top, + ).alignment, + Alignment.topCenter, + ); + expect( + PopupLayout( + placement: TPopupPlacement.right, + ).alignment, + Alignment.centerRight, + ); + }); + }); +} diff --git a/tdesign-component/test/t_popup_options_test.dart b/tdesign-component/test/t_popup_options_test.dart new file mode 100644 index 000000000..55c7d4982 --- /dev/null +++ b/tdesign-component/test/t_popup_options_test.dart @@ -0,0 +1,389 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +void main() { + group('TPopupOptions', () { + test('默认 placement 为 bottom,4 个 builder 默认 sentinel', () { + final options = TPopupOptions(child: const SizedBox()).normalized(); + expect(options.placement, TPopupPlacement.bottom); + expect(options.modal, isTrue); + expect(options.closeOnOverlayClick, isTrue); + expect(options.usesDefaultHeader, isTrue); + expect(options.usesDefaultCancel, isTrue); + expect(options.usesDefaultConfirm, isTrue); + expect(options.titleWidget, isNull); + }); + + test('showOverlay=false 且省略 closeOnOverlayClick 时默认按 false 解析', () { + final options = TPopupOptions( + child: const SizedBox(), + showOverlay: false, + ).normalized(); + expect(options.closeOnOverlayClick, isFalse); + }); + + test('模态与蒙层合法组合矩阵可通过校验并解析默认关闭行为', () { + void expectValid( + String reason, + TPopupOptions options, { + required bool expectedCloseOnOverlayClick, + }) { + expect( + () => options.assertPlacementParams(), + returnsNormally, + reason: reason, + ); + expect( + options.closeOnOverlayClick, + expectedCloseOnOverlayClick, + reason: reason, + ); + } + + expectValid( + '标准模态 + 默认蒙层关闭', + TPopupOptions( + child: const SizedBox(), + showOverlay: true, + modal: true, + ), + expectedCloseOnOverlayClick: true, + ); + expectValid( + '标准模态 + 显式禁止蒙层关闭', + TPopupOptions( + child: const SizedBox(), + showOverlay: true, + modal: true, + closeOnOverlayClick: false, + ), + expectedCloseOnOverlayClick: false, + ); + expectValid( + '透明模态 + 默认不允许蒙层关闭', + TPopupOptions( + child: const SizedBox(), + showOverlay: false, + modal: true, + ), + expectedCloseOnOverlayClick: false, + ); + expectValid( + '透明模态 + 显式 false 仍合法', + TPopupOptions( + child: const SizedBox(), + showOverlay: false, + modal: true, + closeOnOverlayClick: false, + ), + expectedCloseOnOverlayClick: false, + ); + expectValid( + '非模态浮层 + 默认不允许蒙层关闭', + TPopupOptions( + child: const SizedBox(), + showOverlay: false, + modal: false, + ), + expectedCloseOnOverlayClick: false, + ); + expectValid( + '非模态浮层 + 显式 false 仍合法', + TPopupOptions( + child: const SizedBox(), + showOverlay: false, + modal: false, + closeOnOverlayClick: false, + ), + expectedCloseOnOverlayClick: false, + ); + }); + + test('bottom 默认走内置三段式(useDefaultHeader)', () { + final options = TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.bottom, + ).normalized(); + expect(options.useDefaultHeader, isTrue); + expect(options.useCustomHeader, isFalse); + expect(options.showCancelSlot, isTrue); + expect(options.showConfirmSlot, isTrue); + expect(options.hasBuiltInHeader, isTrue); + }); + + test('cancelBuilder / confirmBuilder 均为 null 时槽位隐藏', () { + final options = TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.bottom, + cancelBuilder: null, + confirmBuilder: null, + ).normalized(); + expect(options.showCancelSlot, isFalse); + expect(options.showConfirmSlot, isFalse); + expect(options.hasBuiltInHeader, isFalse); // titleWidget 也为 null + }); + + test('headerBuilder null 不显示头部', () { + final options = TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.bottom, + headerBuilder: null, + ).normalized(); + expect(options.useDefaultHeader, isFalse); + expect(options.useCustomHeader, isFalse); + expect(options.hasBuiltInHeader, isFalse); + }); + + test('headerBuilder 自定义 → useCustomHeader 为 true', () { + final options = TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.bottom, + headerBuilder: (_, __) => const SizedBox(), + ).normalized(); + expect(options.useCustomHeader, isTrue); + expect(options.hasBuiltInHeader, isTrue); + }); + + test('normalized 忽略 bottom 的 closeBuilder(非 sentinel 也置 null)', () { + final options = TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.bottom, + closeBuilder: (_, __) => const Text('x'), + ).normalized(); + expect(options.closeBuilder, isNull); + }); + + test('center 默认 closeBuilder 为 sentinel(内置图标)', () { + final options = TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.center, + ).normalized(); + expect(options.usesDefaultClose, isTrue); + }); + + test('center closeBuilder=null 不显示关闭区', () { + final options = TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.center, + closeBuilder: null, + ).normalized(); + expect(options.closeBuilder, isNull); + }); + + test('top 剥离 header 与三槽,重置为 null', () { + final options = TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.top, + titleWidget: const Text('x'), + headerBuilder: (_, __) => const Text('h'), + ).normalized(); + expect(options.headerBuilder, isNull); + expect(options.titleWidget, isNull); + expect(options.cancelBuilder, isNull); + expect(options.confirmBuilder, isNull); + expect(options.hasBuiltInHeader, isFalse); + }); + + test('left/right 剥离 closeBuilder', () { + final left = TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.left, + closeBuilder: (_, __) => const Text('x'), + ).normalized(); + expect(left.closeBuilder, isNull); + }); + + test('hasBuiltInHeader 内置三段中任一槽非 null 即 true', () { + // titleWidget 单独存在 + expect( + TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.bottom, + titleWidget: const Text('x'), + cancelBuilder: null, + confirmBuilder: null, + ).normalized().hasBuiltInHeader, + isTrue, + ); + // 仅 cancel 默认(其它 null) + expect( + TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.bottom, + confirmBuilder: null, + ).normalized().hasBuiltInHeader, + isTrue, + ); + // 完全无头部 + expect( + TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.bottom, + headerBuilder: null, + ).normalized().hasBuiltInHeader, + isFalse, + ); + // 非 bottom 永远 false + expect( + TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.top, + ).normalized().hasBuiltInHeader, + isFalse, + ); + }); + + test('assertPlacementParams debug 期对错位字段抛 FlutterError', () { + // left 不该有 height + expect( + () => TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.left, + height: 100, + ).assertPlacementParams(), + throwsA(isA()), + ); + // center 不该有 titleWidget(属于 bottom 头部字段) + expect( + () => TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.center, + titleWidget: const Text('x'), + ).assertPlacementParams(), + throwsA(isA()), + ); + expect( + () => TPopupOptions( + child: const SizedBox(), + showOverlay: true, + modal: false, + ).assertPlacementParams(), + throwsA(isA()), + ); + expect( + () => TPopupOptions( + child: const SizedBox(), + showOverlay: false, + closeOnOverlayClick: true, + ).assertPlacementParams(), + throwsA(isA()), + ); + }); + + test('模态与蒙层非法组合矩阵全部抛 FlutterError', () { + void expectInvalid(String reason, TPopupOptions options) { + expect( + () => options.assertPlacementParams(), + throwsA(isA()), + reason: reason, + ); + } + + expectInvalid( + '有蒙层但非模态(默认蒙层关闭值)', + TPopupOptions( + child: const SizedBox(), + showOverlay: true, + modal: false, + ), + ); + expectInvalid( + '有蒙层但非模态(显式 false)', + TPopupOptions( + child: const SizedBox(), + showOverlay: true, + modal: false, + closeOnOverlayClick: false, + ), + ); + expectInvalid( + '透明模态下显式要求蒙层关闭', + TPopupOptions( + child: const SizedBox(), + showOverlay: false, + modal: true, + closeOnOverlayClick: true, + ), + ); + expectInvalid( + '非模态浮层下显式要求蒙层关闭', + TPopupOptions( + child: const SizedBox(), + showOverlay: false, + modal: false, + closeOnOverlayClick: true, + ), + ); + }); + + test('assertPlacementParams 各 placement 的 inset 类型错位也抛错', () { + expect( + () => TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.top, + inset: const TPopupBottomInset(left: 10), + ).assertPlacementParams(), + throwsA(isA()), + ); + expect( + () => TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.right, + inset: const TPopupLeftInset(top: 10), + ).assertPlacementParams(), + throwsA(isA()), + ); + expect( + () => TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.center, + inset: const TPopupTopInset(left: 10), + ).assertPlacementParams(), + throwsA(isA()), + ); + }); + + test('assertPlacementParams 合法配置不抛错', () { + // 各 placement 用对应合法字段 + expect( + () => TPopupOptions(child: const SizedBox()).assertPlacementParams(), + returnsNormally); + expect( + () => TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.center, + width: 200, + height: 200, + ).assertPlacementParams(), + returnsNormally, + ); + expect( + () => TPopupOptions( + child: const SizedBox(), + placement: TPopupPlacement.left, + width: 280, + inset: const TPopupLeftInset(top: 10, bottom: 10), + ).assertPlacementParams(), + returnsNormally, + ); + expect( + () => TPopupOptions( + child: const SizedBox(), + showOverlay: false, + modal: false, + ).assertPlacementParams(), + returnsNormally, + ); + }); + + test('copyWith(closeOnOverlayClick: null) 可恢复为跟随 showOverlay 的默认值', () { + final options = TPopupOptions( + child: const SizedBox(), + showOverlay: false, + closeOnOverlayClick: false, + ).copyWith(closeOnOverlayClick: null); + expect(options.closeOnOverlayClick, isFalse); + }); + }); +} diff --git a/tdesign-component/test/t_popup_route_test.dart b/tdesign-component/test/t_popup_route_test.dart new file mode 100644 index 000000000..93253f90b --- /dev/null +++ b/tdesign-component/test/t_popup_route_test.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +import 'helpers/popup_test_helpers.dart'; +import 'helpers/popup_test_resource.dart'; + +void main() { + tearDown(resetPopupTestResource); + + group('Popup 路由层行为(通过 TPopup.show 验证)', () { + testWidgets('无蒙层 + modal=true 时阻断底层点击与滚动', + (tester) async { + final controller = ScrollController(); + var backgroundTapCount = 0; + + await tester.pumpWidget( + MaterialApp( + home: TTheme( + data: TThemeData.defaultData(), + child: Scaffold( + body: Builder( + builder: (context) { + return Column( + children: [ + ElevatedButton( + onPressed: () => backgroundTapCount++, + child: const Text('background button'), + ), + Expanded( + child: ListView.builder( + controller: controller, + itemCount: 30, + itemBuilder: (_, index) => SizedBox( + height: 48, + child: Text('item-$index'), + ), + ), + ), + ElevatedButton( + onPressed: () { + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 120, + showOverlay: false, + modal: true, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 60), + ), + ); + }, + child: const Text('open popup'), + ), + ], + ); + }, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + final backgroundButtonCenter = + tester.getCenter(find.text('background button')); + final listDragStart = tester.getCenter(find.text('item-3')); + + await tester.tap(find.text('open popup')); + await tester.pumpAndSettle(); + + await tester.tapAt(backgroundButtonCenter); + await tester.pump(); + expect(backgroundTapCount, 0); + + await tester.dragFrom(listDragStart, const Offset(0, -200)); + await tester.pump(); + expect(controller.offset, 0); + }); + + testWidgets('无蒙层 + modal=false 时允许底层点击与滚动', + (tester) async { + final controller = ScrollController(); + var backgroundTapCount = 0; + + await tester.pumpWidget( + MaterialApp( + home: TTheme( + data: TThemeData.defaultData(), + child: Scaffold( + body: Builder( + builder: (context) { + return Column( + children: [ + ElevatedButton( + onPressed: () => backgroundTapCount++, + child: const Text('background button'), + ), + Expanded( + child: ListView.builder( + controller: controller, + itemCount: 30, + itemBuilder: (_, index) => SizedBox( + height: 48, + child: Text('item-$index'), + ), + ), + ), + ElevatedButton( + onPressed: () { + TPopup.show( + context, + options: TPopupOptions.bottom( + height: 120, + showOverlay: false, + modal: false, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 60), + ), + ); + }, + child: const Text('open popup'), + ), + ], + ); + }, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + final backgroundButtonCenter = + tester.getCenter(find.text('background button')); + final listDragStart = tester.getCenter(find.text('item-3')); + + await tester.tap(find.text('open popup')); + await tester.pumpAndSettle(); + + await tester.tapAt(backgroundButtonCenter); + await tester.pump(); + expect(backgroundTapCount, 1); + + await tester.dragFrom(listDragStart, const Offset(0, -200)); + await tester.pump(); + expect(controller.offset, greaterThan(0)); + }); + + testWidgets('fireCloseStart 仅触发一次 onClose', (tester) async { + var closeCount = 0; + late BuildContext hostContext; + TPopupHandle? handle; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + handle = TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 100, + onClose: () => closeCount++, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + handle!.close(); + await tester.pumpAndSettle(); + expect(closeCount, 1); + }); + }); +} diff --git a/tdesign-component/test/t_popup_test.dart b/tdesign-component/test/t_popup_test.dart new file mode 100644 index 000000000..1191a3c03 --- /dev/null +++ b/tdesign-component/test/t_popup_test.dart @@ -0,0 +1,1507 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +import 'helpers/popup_test_helpers.dart'; +import 'helpers/popup_test_resource.dart'; + +class _RecordingNavigatorObserver extends NavigatorObserver { + final List> pushedRoutes = >[]; + + @override + void didPush(Route route, Route? previousRoute) { + pushedRoutes.add(route); + super.didPush(route, previousRoute); + } + + int get popupPushCount => + pushedRoutes.where((route) => route is PopupRoute).length; +} + +void main() { + tearDown(resetPopupTestResource); + + group('TPopup 国际化文案', () { + for (final resource in [ + PopupTestResourceDelegate.zh(), + PopupTestResourceDelegate.en(), + ]) { + testWidgets( + '${resource.locale.languageCode} 底部默认 cancel / confirm 按钮点击关闭浮层', + (tester) async { + late BuildContext hostContext; + + await openPopup( + tester, + resource: resource, + onPressed: () { + hostContext = tester.element(find.text('open')); + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 200, + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text(resource.cancelText), findsOneWidget); + expect(find.text(resource.confirmText), findsOneWidget); + + await tester.tap(find.text(resource.cancelText)); + await tester.pumpAndSettle(); + expect(find.text(resource.cancelText), findsNothing); + + await openPopup( + tester, + resource: resource, + onPressed: () { + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 200, + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text(resource.confirmText)); + await tester.pumpAndSettle(); + expect(find.text(resource.confirmText), findsNothing); + }, + ); + } + + testWidgets('同一用例内切换中英文资源', (tester) async { + late BuildContext hostContext; + + for (final resource in [ + PopupTestResourceDelegate.zh(), + PopupTestResourceDelegate.en(), + ]) { + TPopupHandle? handle; + await openPopup( + tester, + resource: resource, + onPressed: () { + hostContext = tester.element(find.text('open')); + handle = TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text(resource.cancelText), findsOneWidget); + expect(find.text(resource.confirmText), findsOneWidget); + handle!.close(); + await tester.pumpAndSettle(); + } + }); + }); + + group('TPopup 生命周期', () { + testWidgets('show 触发 onOpen / onOpened 各一次', (tester) async { + var openCount = 0; + var openedCount = 0; + late BuildContext hostContext; + TPopupHandle? handle; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + handle = TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 120, + onOpen: () => openCount++, + onOpened: () => openedCount++, + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pump(); + expect(openCount, 1); + await tester.pumpAndSettle(); + expect(openedCount, 1); + + handle!.close(); + await tester.pumpAndSettle(); + }); + + testWidgets('handle.close 触发 onClose / onClosed', (tester) async { + var closeCount = 0; + var closedCount = 0; + var visibleChanges = []; + late BuildContext hostContext; + TPopupHandle? handle; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + handle = TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 120, + onClose: () => closeCount++, + onClosed: () => closedCount++, + onVisibleChange: (v, _) => visibleChanges.add(v), + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + handle!.close(); + await tester.pumpAndSettle(); + expect(closeCount, 1); + expect(closedCount, 1); + expect(visibleChanges, [true, false]); + expect(handle!.isShowing, false); + }); + + testWidgets('重复 close 不会重复触发 onClose', (tester) async { + var closeCount = 0; + late BuildContext hostContext; + TPopupHandle? handle; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + handle = TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 100, + onClose: () => closeCount++, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + handle!.close(); + handle!.close(); + await tester.pumpAndSettle(); + expect(closeCount, 1); + }); + }); + + group('TPopup Navigator 选择', () { + testWidgets('navigatorContext 首次 show 挂载到指定 Navigator,且 handle.open 复用缓存', + (tester) async { + final rootObserver = _RecordingNavigatorObserver(); + final nestedObserver = _RecordingNavigatorObserver(); + late BuildContext rootContext; + late BuildContext nestedContext; + late TPopupHandle handle; + + await tester.pumpWidget( + MaterialApp( + navigatorObservers: [rootObserver], + home: TTheme( + data: TThemeData.defaultData(), + child: Builder( + builder: (rootCtx) { + rootContext = rootCtx; + return Navigator( + observers: [nestedObserver], + onGenerateRoute: (_) { + return MaterialPageRoute( + builder: (nestedCtx) { + nestedContext = nestedCtx; + return Scaffold( + body: ElevatedButton( + onPressed: () { + handle = TPopup.show( + nestedContext, + navigatorContext: rootContext, + options: TPopupOptions.bottom( + height: 120, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox( + height: 60, + child: Text('root popup'), + ), + ), + ); + }, + child: const Text('open via root'), + ), + ); + }, + ); + }, + ); + }, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('open via root')); + await tester.pumpAndSettle(); + expect(rootObserver.popupPushCount, 1); + expect(nestedObserver.popupPushCount, 0); + expect(find.text('root popup'), findsOneWidget); + + handle.close(); + await tester.pumpAndSettle(); + expect(find.text('root popup'), findsNothing); + + handle.open(); + await tester.pumpAndSettle(); + expect(rootObserver.popupPushCount, 2); + expect(nestedObserver.popupPushCount, 0); + expect(find.text('root popup'), findsOneWidget); + }); + + testWidgets('useRootNavigator=true 会挂载到根 Navigator', (tester) async { + final rootObserver = _RecordingNavigatorObserver(); + final nestedObserver = _RecordingNavigatorObserver(); + late BuildContext nestedContext; + + await tester.pumpWidget( + MaterialApp( + navigatorObservers: [rootObserver], + home: TTheme( + data: TThemeData.defaultData(), + child: Navigator( + observers: [nestedObserver], + onGenerateRoute: (_) { + return MaterialPageRoute( + builder: (ctx) { + nestedContext = ctx; + return Scaffold( + body: ElevatedButton( + onPressed: () { + TPopup.show( + nestedContext, + useRootNavigator: true, + options: TPopupOptions.center( + width: 120, + height: 80, + closeBuilder: null, + child: const SizedBox( + width: 120, + height: 80, + child: Text('root navigator popup'), + ), + ), + ); + }, + child: const Text('open root navigator'), + ), + ); + }, + ); + }, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('open root navigator')); + await tester.pumpAndSettle(); + expect(rootObserver.popupPushCount, 1); + expect(nestedObserver.popupPushCount, 0); + expect(find.text('root navigator popup'), findsOneWidget); + }); + }); + + group('TPopup placement', () { + const placements = [ + TPopupPlacement.top, + TPopupPlacement.bottom, + TPopupPlacement.left, + TPopupPlacement.right, + TPopupPlacement.center, + ]; + for (final placement in placements) { + testWidgets('$placement 可正常打开关闭', (tester) async { + late BuildContext hostContext; + TPopupHandle? handle; + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + handle = TPopup.show( + hostContext, + options: TPopupOptions( + placement: placement, + height: placement == TPopupPlacement.left || + placement == TPopupPlacement.right + ? null + : 120, + width: placement == TPopupPlacement.top || + placement == TPopupPlacement.bottom + ? null + : 200, + child: const SizedBox(height: 60, width: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.byType(Stack), findsWidgets); + handle!.close(); + await tester.pumpAndSettle(); + }); + } + }); + + group('TPopup Header', () { + testWidgets('底部默认头部:title / cancel / confirm 按钮渲染', (tester) async { + late BuildContext hostContext; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 200, + titleWidget: TText('标题'), + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('标题'), findsOneWidget); + expect(find.text('取消'), findsOneWidget); + expect(find.text('确定'), findsOneWidget); + + // 点取消默认 sentinel 自带 close → 浮层关闭 + await tester.tap(find.text('取消')); + await tester.pumpAndSettle(); + expect(find.text('标题'), findsNothing); + + // 重新打开点确定 → 同样关闭 + await openPopup( + tester, + onPressed: () { + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 200, + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('确定')); + await tester.pumpAndSettle(); + expect(find.text('确定'), findsNothing); + }); + + testWidgets('cancelBuilder 自定义可选择不调 close → 浮层保持展示', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 200, + cancelBuilder: (_, __) => GestureDetector( + onTap: () {}, // 不调 close + child: const Text('自定义取消'), + ), + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('自定义取消')); + await tester.pump(); + expect(find.text('自定义取消'), findsOneWidget); + }); + + testWidgets('bottom headerBuilder=null 不渲染头部', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 180, + titleWidget: TText('不应出现'), + headerBuilder: null, + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('不应出现'), findsNothing); + expect(find.text('取消'), findsNothing); + }); + + testWidgets('bottom 仅标题无操作栏', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 180, + titleWidget: TText('仅标题'), + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('仅标题'), findsOneWidget); + expect(find.text('取消'), findsNothing); + expect(find.text('确定'), findsNothing); + expect(find.byIcon(TIcons.close), findsNothing); + }); + + testWidgets('headerBuilder 自定义头部', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + headerBuilder: (_, __) => const Text('自定义:传入标题'), + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.textContaining('自定义'), findsOneWidget); + }); + + testWidgets('居中关闭在内容与下方', (tester) async { + late BuildContext hostContext; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.center, + width: 120, + height: 120, + child: const SizedBox(height: 80, width: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.byIcon(TIcons.close_circle), findsOneWidget); + await tester.tap(find.byIcon(TIcons.close_circle)); + await tester.pumpAndSettle(); + }); + + testWidgets('居中关闭在下方与示例一致 expand 不报错', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.center, + width: 240, + height: 240, + child: const SizedBox.expand()), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.byIcon(TIcons.close_circle), findsOneWidget); + }); + + testWidgets('center closeBuilder=null 不显示关闭区', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.center, + width: 120, + height: 120, + closeBuilder: null, + child: const SizedBox(height: 80, width: 80)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.byIcon(TIcons.close_circle), findsNothing); + }); + + testWidgets('cancelBuilder / confirmBuilder', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + cancelBuilder: (_, __) => const Text('自定义取消'), + confirmBuilder: (_, __) => const Text('自定义确认'), + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('自定义取消'), findsOneWidget); + expect(find.text('自定义确认'), findsOneWidget); + }); + }); + + group('TPopup 蒙层与行为', () { + testWidgets('点击蒙层关闭', (tester) async { + var overlayClose = 0; + late BuildContext hostContext; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 100, + onClosed: () => overlayClose++, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + // 点击蒙层区域(屏幕上方) + await tester.tapAt(const Offset(10, 10)); + await tester.pumpAndSettle(); + expect(overlayClose, 1); + }); + + testWidgets('closeOnOverlayClick 为 false 点击蒙层不关闭', (tester) async { + late BuildContext hostContext; + TPopupHandle? handle; + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + handle = TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 100, + closeOnOverlayClick: false, + onOverlayClick: () {}, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('取消'), findsOneWidget); + await tester.tapAt(const Offset(10, 10)); + await tester.pump(); + expect(find.text('取消'), findsOneWidget); + handle!.close(); + await tester.pumpAndSettle(); + }); + + testWidgets('showOverlay false 且 modal=true', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 100, + showOverlay: false, + modal: true, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect( + find.byType(ModalBarrier), + findsWidgets, + ); + }); + + testWidgets('模态与蒙层合法组合矩阵都可正常 show / close', (tester) async { + late BuildContext hostContext; + + await tester.pumpWidget( + wrapPopupTest( + Builder( + builder: (context) { + hostContext = context; + return const SizedBox.shrink(); + }, + ), + ), + ); + + Future expectShowAndClose( + String label, + TPopupOptions options, + ) async { + late TPopupHandle handle; + expect( + () => handle = TPopup.show(hostContext, options: options), + returnsNormally, + reason: label, + ); + await tester.pumpAndSettle(); + expect(find.text(label), findsOneWidget, reason: label); + + handle.close(); + await tester.pumpAndSettle(); + expect(find.text(label), findsNothing, reason: label); + } + + await expectShowAndClose( + '标准模态默认关闭', + TPopupOptions.bottom( + height: 100, + showOverlay: true, + modal: true, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 60, child: Text('标准模态默认关闭')), + ), + ); + await expectShowAndClose( + '标准模态显式禁止蒙层关闭', + TPopupOptions.bottom( + height: 100, + showOverlay: true, + modal: true, + closeOnOverlayClick: false, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 60, child: Text('标准模态显式禁止蒙层关闭')), + ), + ); + await expectShowAndClose( + '透明模态默认关闭策略', + TPopupOptions.bottom( + height: 100, + showOverlay: false, + modal: true, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 60, child: Text('透明模态默认关闭策略')), + ), + ); + await expectShowAndClose( + '非模态浮层默认关闭策略', + TPopupOptions.bottom( + height: 100, + showOverlay: false, + modal: false, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 60, child: Text('非模态浮层默认关闭策略')), + ), + ); + }); + + testWidgets('overlayOpacity 与自定义颜色', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 80, + overlayColor: Colors.red, + overlayOpacity: 0.5, + child: const SizedBox(height: 40)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.pump(); + }); + + testWidgets('right inset.top 可控制顶部留白', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.right, + inset: const TPopupRightInset(top: 80), + child: const SizedBox(height: 200)), + ); + }, + ); + await tester.pumpAndSettle(); + }); + }); + + group('TPopupHandle / Tracker', () { + testWidgets('重复 show 可叠加打开(返回不同 handle)', (tester) async { + TPopupHandle? first; + TPopupHandle? second; + await openPopup( + tester, + onPressed: () { + final ctx = tester.element(find.text('open')); + first = TPopup.show( + ctx, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 80, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 40, child: Text('first'))), + ); + second = TPopup.show( + ctx, + options: TPopupOptions( + placement: TPopupPlacement.center, + width: 120, + height: 80, + closeBuilder: null, + child: const SizedBox(width: 120, height: 80, child: Text('second'))), + ); + expect(second!.isShowing, isTrue); + expect(identical(first, second), isFalse); + }, + ); + await tester.pumpAndSettle(); + expect(first!.isShowing, isTrue); + expect(second!.isShowing, isTrue); + expect(find.text('first'), findsOneWidget); + expect(find.text('second'), findsOneWidget); + + second!.close(); + await tester.pumpAndSettle(); + expect(second!.isShowing, isFalse); + expect(first!.isShowing, isTrue); + expect(find.text('second'), findsNothing); + expect(find.text('first'), findsOneWidget); + + first!.close(); + await tester.pumpAndSettle(); + }); + + testWidgets('外层 handle.close 在内层展示时只关闭外层', (tester) async { + TPopupHandle? outerHandle; + TPopupHandle? innerHandle; + + await openPopup( + tester, + onPressed: () { + outerHandle = TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 160, + cancelBuilder: null, + confirmBuilder: null, + child: Builder( + builder: (ctx) { + return Column( + children: [ + const Text('outer'), + ElevatedButton( + onPressed: () { + innerHandle = TPopup.show( + ctx, + options: TPopupOptions( + placement: TPopupPlacement.center, + width: 120, + height: 80, + closeBuilder: null, + child: const SizedBox( + width: 120, + height: 80, + child: Text('inner'), + ), + ), + ); + }, + child: const Text('open inner'), + ), + ], + ); + }, + )), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('outer'), findsOneWidget); + + await tester.tap(find.text('open inner')); + await tester.pumpAndSettle(); + expect(find.text('outer'), findsOneWidget); + expect(find.text('inner'), findsOneWidget); + + outerHandle!.close(); + await tester.pumpAndSettle(); + expect(outerHandle!.isShowing, isFalse); + expect(innerHandle!.isShowing, isTrue); + expect(find.text('outer'), findsNothing); + expect(find.text('inner'), findsOneWidget); + + innerHandle!.close(); + await tester.pumpAndSettle(); + expect(find.text('inner'), findsNothing); + }); + + testWidgets('系统返回键关闭并上报 systemBack trigger', (tester) async { + var closedCount = 0; + TPopupTrigger? hideTrigger; + late BuildContext hostContext; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 100, + onClosed: () => closedCount++, + onVisibleChange: (visible, trigger) { + if (!visible) { + hideTrigger = trigger; + } + }, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.binding.handlePopRoute(); + await tester.pumpAndSettle(); + expect(closedCount, 1); + expect(hideTrigger, TPopupTrigger.systemBack); + }); + + testWidgets('handle.close 后 handle.open 可再次展示', (tester) async { + late BuildContext hostContext; + TPopupHandle? handle; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + handle = TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 100, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 60, child: Text('panel'))), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('panel'), findsOneWidget); + + handle!.close(); + await tester.pumpAndSettle(); + expect(handle!.isShowing, isFalse); + expect(find.text('panel'), findsNothing); + + handle!.open(hostContext); + await tester.pumpAndSettle(); + expect(handle!.isShowing, isTrue); + expect(find.text('panel'), findsOneWidget); + }); + + testWidgets('关闭动画未结束时重新 open 不会被旧 route 回调误清理', (tester) async { + late BuildContext hostContext; + TPopupHandle? handle; + var openCount = 0; + var closedCount = 0; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + handle = TPopup.show( + hostContext, + options: TPopupOptions.bottom( + height: 100, + cancelBuilder: null, + confirmBuilder: null, + animationDuration: const Duration(milliseconds: 300), + onOpen: () => openCount++, + onClosed: () => closedCount++, + child: const SizedBox(height: 60, child: Text('race panel')), + ), + ); + }, + ); + await tester.pumpAndSettle(); + expect(handle!.isShowing, isTrue); + expect(openCount, 1); + expect(closedCount, 0); + + handle!.close(); + await tester.pump(const Duration(milliseconds: 100)); + + handle!.open(hostContext); + await tester.pump(); + expect(handle!.isShowing, isTrue); + expect(openCount, 2); + + await tester.pumpAndSettle(); + expect(handle!.isShowing, isTrue); + expect(find.text('race panel'), findsOneWidget); + expect(closedCount, 0); + + handle!.close(); + await tester.pumpAndSettle(); + expect(closedCount, 1); + }); + + testWidgets('navigatorContext 失效后 handle.open 仍优先复用缓存 navigator', + (tester) async { + TPopupHandle? handle; + var showLauncher = true; + + await tester.pumpWidget( + MaterialApp( + home: TTheme( + data: TThemeData.defaultData(), + child: StatefulBuilder( + builder: (context, setState) { + return Scaffold( + body: Column( + children: [ + if (showLauncher) + Builder( + builder: (launcherContext) { + return ElevatedButton( + onPressed: () { + handle = TPopup.show( + launcherContext, + navigatorContext: launcherContext, + options: TPopupOptions.bottom( + height: 100, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox( + height: 60, + child: Text('cached navigator popup'), + ), + ), + ); + }, + child: const Text('open popup'), + ); + }, + ), + ElevatedButton( + onPressed: () { + setState(() => showLauncher = false); + }, + child: const Text('dispose launcher'), + ), + ], + ), + ); + }, + ), + ), + ), + ); + await tester.tap(find.text('open popup')); + await tester.pumpAndSettle(); + expect(find.text('cached navigator popup'), findsOneWidget); + + handle!.close(); + await tester.pumpAndSettle(); + expect(find.text('cached navigator popup'), findsNothing); + + await tester.tap(find.text('dispose launcher')); + await tester.pumpAndSettle(); + + expect(() => handle!.open(), returnsNormally); + await tester.pumpAndSettle(); + expect(handle!.isShowing, isTrue); + expect(find.text('cached navigator popup'), findsOneWidget); + }); + + testWidgets('非栈顶 handle.close 会立即移除 route 并触发 onClosed', (tester) async { + TPopupHandle? outerHandle; + TPopupHandle? innerHandle; + var outerClosedCount = 0; + + await openPopup( + tester, + onPressed: () { + outerHandle = TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions.bottom( + height: 160, + animationDuration: const Duration(milliseconds: 300), + cancelBuilder: null, + confirmBuilder: null, + onClosed: () => outerClosedCount++, + child: Builder( + builder: (ctx) { + return Column( + children: [ + const Text('outer immediate remove'), + ElevatedButton( + onPressed: () { + innerHandle = TPopup.show( + ctx, + options: TPopupOptions.center( + width: 120, + height: 80, + closeBuilder: null, + child: const SizedBox( + width: 120, + height: 80, + child: Text('inner immediate remove'), + ), + ), + ); + }, + child: const Text('open inner immediate'), + ), + ], + ); + }, + ), + ), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('open inner immediate')); + await tester.pumpAndSettle(); + + outerHandle!.close(); + await tester.pump(); + expect(find.text('outer immediate remove'), findsNothing); + expect(find.text('inner immediate remove'), findsOneWidget); + expect(outerClosedCount, 1); + + innerHandle!.close(); + await tester.pumpAndSettle(); + }); + + testWidgets('handle.open 在已展示时无副作用', (tester) async { + late BuildContext hostContext; + TPopupHandle? handle; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + handle = TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 80, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 40)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(handle!.isShowing, isTrue); + + handle!.open(hostContext); + await tester.pumpAndSettle(); + expect(handle!.isShowing, isTrue); + }); + + testWidgets('非法参数组合矩阵在 show 时直接抛 FlutterError', (tester) async { + late BuildContext hostContext; + + await tester.pumpWidget( + wrapPopupTest( + Builder( + builder: (context) { + hostContext = context; + return const SizedBox.shrink(); + }, + ), + ), + ); + + void expectInvalidShow(String reason, TPopupOptions options) { + expect( + () => TPopup.show(hostContext, options: options), + throwsA(isA()), + reason: reason, + ); + } + + expectInvalidShow( + '有蒙层但非模态(默认关闭策略)', + TPopupOptions.bottom( + child: const SizedBox(height: 40), + showOverlay: true, + modal: false, + ), + ); + expectInvalidShow( + '有蒙层但非模态(显式 false)', + TPopupOptions.bottom( + child: const SizedBox(height: 40), + showOverlay: true, + modal: false, + closeOnOverlayClick: false, + ), + ); + expectInvalidShow( + '透明模态显式要求蒙层关闭', + TPopupOptions.bottom( + child: const SizedBox(height: 40), + showOverlay: false, + modal: true, + closeOnOverlayClick: true, + ), + ); + expectInvalidShow( + '非模态浮层显式要求蒙层关闭', + TPopupOptions.bottom( + child: const SizedBox(height: 40), + showOverlay: false, + modal: false, + closeOnOverlayClick: true, + ), + ); + }); + }); + + group('TPopup 扩展场景', () { + testWidgets('top 不渲染头部、仅显示 child(用 .top factory)', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + // .top factory 根本不暴露 titleWidget:编译期就杜绝错位 + options: TPopupOptions.top( + height: 120, + child: const Text('内容'), + ), + ); + }, + ); + await tester.pumpAndSettle(); + // 不应出现默认头部文案(zh 资源) + expect(find.text('取消'), findsNothing); + expect(find.text('确定'), findsNothing); + expect(find.text('内容'), findsOneWidget); + }); + + testWidgets('left / right 侧栏展开内容', (tester) async { + for (final placement in [ + TPopupPlacement.left, + TPopupPlacement.right, + ]) { + late BuildContext hostContext; + TPopupHandle? handle; + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + handle = TPopup.show( + hostContext, + options: TPopupOptions( + placement: placement, + width: 240, + child: const SizedBox(height: 40)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.byType(Expanded), findsOneWidget); + handle!.close(); + await tester.pumpAndSettle(); + } + }); + + testWidgets('titleWidget + 自定义 cancel/confirm Builder 文案', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 180, + titleWidget: const Text('Widget标题'), + cancelBuilder: (_, close) => + GestureDetector(onTap: close, child: const Text('左')), + confirmBuilder: (_, close) => + GestureDetector(onTap: close, child: const Text('右')), + child: const SizedBox(height: 40)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('Widget标题'), findsOneWidget); + expect(find.text('左'), findsOneWidget); + expect(find.text('右'), findsOneWidget); + }); + + testWidgets('destroyOnClose 与自定义 close 组件', (tester) async { + late BuildContext hostContext; + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.center, + width: 140, + destroyOnClose: true, + closeBuilder: (_, close) => GestureDetector( + onTap: close, + child: const Text('关'), + ), + child: const SizedBox(height: 60, width: 120)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('关')); + await tester.pumpAndSettle(); + }); + + testWidgets('onOverlayClick 且点击蒙层关闭', (tester) async { + var overlayClick = 0; + late BuildContext hostContext; + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 100, + onOverlayClick: () => overlayClick++, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tapAt(const Offset(10, 10)); + await tester.pumpAndSettle(); + expect(overlayClick, 1); + }); + + testWidgets('show 返回的 handle 关闭后 isShowing 为 false', (tester) async { + TPopupHandle? handle; + await openPopup( + tester, + onPressed: () { + handle = TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 80, + child: const SizedBox(height: 40)), + ); + }, + ); + await tester.pumpAndSettle(); + handle!.close(); + await tester.pumpAndSettle(); + expect(handle!.isShowing, isFalse); + handle!.close(); + }); + }); + + group('TPopup 触发源与配置', () { + testWidgets('handle.close 触发 api', (tester) async { + TPopupTrigger? hideTrigger; + TPopupHandle? handle; + + await openPopup( + tester, + onPressed: () { + handle = TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 100, + onVisibleChange: (v, t) { + if (!v) { + hideTrigger = t; + } + }, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + handle!.close(); + await tester.pumpAndSettle(); + expect(hideTrigger, TPopupTrigger.api); + }); + + testWidgets('confirm 点击触发 confirm', (tester) async { + TPopupTrigger? hideTrigger; + late BuildContext hostContext; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 120, + onVisibleChange: (v, t) { + if (!v) { + hideTrigger = t; + } + }, + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('确定')); + await tester.pumpAndSettle(); + expect(hideTrigger, TPopupTrigger.confirm); + }); + + testWidgets('destroyOnClose 路由关闭后可再次 show', (tester) async { + late BuildContext hostContext; + TPopupHandle? first; + + await openPopup( + tester, + onPressed: () { + hostContext = tester.element(find.text('open')); + first = TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 80, + destroyOnClose: true, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 40)), + ); + }, + ); + await tester.pumpAndSettle(); + first!.close(); + await tester.pumpAndSettle(); + expect(first!.isShowing, isFalse); + + TPopupHandle? second; + await openPopup( + tester, + onPressed: () { + second = TPopup.show( + hostContext, + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 80, + destroyOnClose: true, + cancelBuilder: null, + confirmBuilder: null, + child: const SizedBox(height: 40)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(second!.isShowing, isTrue); + second!.close(); + await tester.pumpAndSettle(); + }); + }); + + group('TPopup 自定义控件', () { + testWidgets('自定义 cancel / confirm / close 组件', (tester) async { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 200, + cancelBuilder: (_, __) => const Text('自定义取消'), + confirmBuilder: (_, __) => const Text('自定义确认'), + child: const SizedBox(height: 60)), + ); + }, + ); + await tester.pumpAndSettle(); + expect(find.text('自定义取消'), findsOneWidget); + expect(find.text('自定义确认'), findsOneWidget); + }); + + testWidgets('自定义 cancelBuilder 保留业务侧语义', (tester) async { + final semanticsHandle = tester.ensureSemantics(); + try { + await openPopup( + tester, + onPressed: () { + TPopup.show( + tester.element(find.text('open')), + options: TPopupOptions( + placement: TPopupPlacement.bottom, + height: 200, + confirmBuilder: null, + cancelBuilder: (_, close) => Semantics( + container: true, + label: '自定义取消语义', + button: true, + child: GestureDetector( + onTap: close, + child: const Text('自定义取消'), + ), + ), + child: const SizedBox(height: 60), + ), + ); + }, + ); + await tester.pumpAndSettle(); + final semanticsNode = tester.getSemantics(find.text('自定义取消')); + expect(semanticsNode.label, contains('自定义取消语义')); + expect(semanticsNode.label, isNot('取消')); + } finally { + semanticsHandle.dispose(); + } + }); + }); +} diff --git a/tdesign-site/.husky/commit-msg b/tdesign-site/.husky/commit-msg deleted file mode 100755 index fe4c17a22..000000000 --- a/tdesign-site/.husky/commit-msg +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx --no-install commitlint --edit "" diff --git a/tdesign-site/.husky/pre-commit b/tdesign-site/.husky/pre-commit deleted file mode 100755 index c37466e2b..000000000 --- a/tdesign-site/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx lint-staged \ No newline at end of file diff --git a/tdesign-site/.husky/prepare-commit-msg b/tdesign-site/.husky/prepare-commit-msg deleted file mode 100755 index 71fe5b35a..000000000 --- a/tdesign-site/.husky/prepare-commit-msg +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -exec < /dev/tty && npx git-cz --hook || true \ No newline at end of file diff --git a/tdesign-site/.templates/$$var_template/$$var_filename.js b/tdesign-site/.templates/$$var_template/$$var_filename.js deleted file mode 100644 index 6420b1220..000000000 --- a/tdesign-site/.templates/$$var_template/$$var_filename.js +++ /dev/null @@ -1 +0,0 @@ -Component({}) \ No newline at end of file diff --git a/tdesign-site/.templates/$$var_template/$$var_filename.json b/tdesign-site/.templates/$$var_template/$$var_filename.json deleted file mode 100644 index 53902317a..000000000 --- a/tdesign-site/.templates/$$var_template/$$var_filename.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "component": true, - "usingComponents": { - "t-$$var_textInFile": "tdesign-miniprogram/$$var_textInFile/$$var_textInFile" - } -} \ No newline at end of file diff --git a/tdesign-site/.templates/$$var_template/$$var_filename.wxml b/tdesign-site/.templates/$$var_template/$$var_filename.wxml deleted file mode 100644 index 427afe3ed..000000000 --- a/tdesign-site/.templates/$$var_template/$$var_filename.wxml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tdesign-site/.templates/$$var_template/$$var_filename.wxss b/tdesign-site/.templates/$$var_template/$$var_filename.wxss deleted file mode 100644 index e69de29bb..000000000 diff --git a/tdesign-site/.templates/.editorconfig b/tdesign-site/.templates/.editorconfig deleted file mode 100644 index 689fb502a..000000000 --- a/tdesign-site/.templates/.editorconfig +++ /dev/null @@ -1,21 +0,0 @@ -# @see https://editorconfig-specification.readthedocs.io/en/latest/ - -# top-most EditorConfig file -root = true - -# Unix-style newlines with a newline ending every file -[*] -end_of_line = lf -insert_final_newline = true -indent_style = space -indent_size = 2 -charset = utf-8 - -# 4 space indentation -[*.py] -indent_style = space -indent_size = 4 - -# Tab indentation (no size specified) -[Makefile] -indent_style = tab diff --git a/tdesign-site/.templates/_example/$$var_filename.js b/tdesign-site/.templates/_example/$$var_filename.js deleted file mode 100644 index 6420b1220..000000000 --- a/tdesign-site/.templates/_example/$$var_filename.js +++ /dev/null @@ -1 +0,0 @@ -Component({}) \ No newline at end of file diff --git a/tdesign-site/.templates/_example/$$var_filename.json b/tdesign-site/.templates/_example/$$var_filename.json deleted file mode 100644 index 32640e0dc..000000000 --- a/tdesign-site/.templates/_example/$$var_filename.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "component": true -} \ No newline at end of file diff --git a/tdesign-site/.templates/_example/$$var_filename.wxml b/tdesign-site/.templates/_example/$$var_filename.wxml deleted file mode 100644 index b1a96ad77..000000000 --- a/tdesign-site/.templates/_example/$$var_filename.wxml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/tdesign-site/.templates/_example/$$var_filename.wxss b/tdesign-site/.templates/_example/$$var_filename.wxss deleted file mode 100644 index e69de29bb..000000000 diff --git a/tdesign-site/.vscode/component.code-snippets b/tdesign-site/.vscode/component.code-snippets deleted file mode 100644 index 16014ccb7..000000000 --- a/tdesign-site/.vscode/component.code-snippets +++ /dev/null @@ -1,14 +0,0 @@ -{ - "component-json-template": { - "prefix": "cjson", - "body": ["{", " \"usingComponents\": {", " \"t-$1\": \"tdeisgn-miniprogram/$1/$1\"", " }", "}"], - "scope": "json", - "description": "组件 JSON" - }, - "component-js-template": { - "prefix": "ccjs", - "body": ["Component({});", ""], - "scope": "javascript, typescript", - "description": "组件 JS" - } -} diff --git a/tdesign-site/.vscode/launch.json b/tdesign-site/.vscode/launch.json deleted file mode 100644 index ec06f201d..000000000 --- a/tdesign-site/.vscode/launch.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "command": "npm run test:unit popup", - "name": "Run unit test", - "request": "launch", - "type": "node-terminal" - }, - ] -} \ No newline at end of file diff --git a/tdesign-site/.vscode/new.code-snippets b/tdesign-site/.vscode/new.code-snippets deleted file mode 100644 index 045b438b0..000000000 --- a/tdesign-site/.vscode/new.code-snippets +++ /dev/null @@ -1,37 +0,0 @@ -{ - "component-json-template": { - "prefix": "newts", - "body": [ - "import { SuperComponent, wxComponent } from '../common/src/index';", - "import config from '../common/config';", - "import props from './props';", - "", - "const { prefix } = config;", - "const name = `\\${prefix}-$1`;", - "", - "@wxComponent()", - "export default class $2 extends SuperComponent {", - " externalClasses = [", - " `\\${prefix}-class`,", - " ];", - "", - " properties = props;", - "", - " observers = {", - " };", - "", - " data = {", - " classPrefix: name,", - " prefix,", - " };", - "", - " methods = {", - " ", - " }", - "}", - "", - ], - "scope": "typescript", - "description": "新组件 ts" - }, -} \ No newline at end of file diff --git a/tdesign-site/.vscode/settings.json b/tdesign-site/.vscode/settings.json deleted file mode 100644 index d764cd678..000000000 --- a/tdesign-site/.vscode/settings.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "editor.formatOnSave": true, - "eslint.enable": true, - "editor.codeActionsOnSave": { - "source.fixAll.tslint": "explicit", - "source.fixAll.eslint": "explicit" - }, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[javascript]": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "minapp-vscode.reserveTags": ["text"], - "files.associations": { - "*.wxss": "css", - "*.wxs": "javascript" - }, - "typescript.tsdk": "node_modules/typescript/lib", - "[json]": { - "editor.defaultFormatter": "vscode.json-language-features" - }, - "minapp-vscode.wxmlFormatter": "prettier", - "[wxml]": { - "editor.defaultFormatter": "qiu8310.minapp-vscode" - }, - "css.validate": false, - "less.validate": false, - "scss.validate": false, - "[less]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "cSpell.words": ["stylelint"] -} diff --git a/tdesign-site/.vscode/test.code-snippets b/tdesign-site/.vscode/test.code-snippets deleted file mode 100644 index 4d9a50ed3..000000000 --- a/tdesign-site/.vscode/test.code-snippets +++ /dev/null @@ -1,31 +0,0 @@ -{ - "test-template": { - "prefix": "ttest", - "body": [ - "import simulate from 'miniprogram-simulate';", - "import path from 'path';", - "", - "describe('$1', () => {", - " const $1 = simulate.load(path.resolve(__dirname, `../$1`), 't-$1', {", - " less: true,", - " rootPath: path.resolve(__dirname, '../..')", - " });", - "", - " it(':$2', () => {", - " const id = simulate.load({", - " template: ``,", - " usingComponents: {", - " 't-$1': $1", - " }", - " })", - " const comp = simulate.render(id);", - " comp.attach(document.createElement('parent-wrapper'));", - "", - " expect(comp.toJSON()).toMatchSnapshot();", - " });", - "});", - ], - "scope": "javascript,typescript", - "description": "测试用例模板" - } -} \ No newline at end of file diff --git a/tdesign-site/commitlint.config.js b/tdesign-site/commitlint.config.js deleted file mode 100644 index 422b19445..000000000 --- a/tdesign-site/commitlint.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = { extends: ['@commitlint/config-conventional'] }; diff --git a/tdesign-site/package.json b/tdesign-site/package.json index f68c4f929..0933ab312 100644 --- a/tdesign-site/package.json +++ b/tdesign-site/package.json @@ -37,7 +37,6 @@ "test:unit": "jest", "test:e2e": "jest -c jest.e2e.config.js", "badge": "node script/coverage-badge.js", - "prepare": "cd .. && husky install tdesign-site/superjsonweb/.husky", "generate": "gulp generate --gulpfile script/gulpfile.js --cwd ./", "changelog": "node script/generate-changelog.js", "robot": "publish-cli robot-msg", @@ -50,8 +49,6 @@ "@babel/plugin-proposal-decorators": "^7.18.9", "@babel/preset-env": "^7.12.11", "@babel/preset-typescript": "^7.12.7", - "@commitlint/cli": "^16.0.2", - "@commitlint/config-conventional": "^16.0.0", "@rollup/plugin-node-resolve": "^13.0.5", "@tdesign/site-components": "^0.18.1", "@types/jest": "^27.0.3", @@ -62,9 +59,7 @@ "@vue/compiler-sfc": "^3.2.4", "axios": "^1.1.3", "babel-jest": "^26.6.3", - "commitizen": "^4.2.4", "cross-env": "^7.0.2", - "cz-conventional-changelog": "^3.3.0", "del": "^6.1.1", "eslint": "^7.0.0", "eslint-config-airbnb-base": "^14.2.1", @@ -84,12 +79,10 @@ "gulp-replace-task": "^2.0.1", "gulp-sourcemaps": "^3.0.0", "gulp-typescript": "^6.0.0-alpha.1", - "husky": "^7.0.4", "jest": "^26.6.3", "jest-html-reporter": "^3.3.0", "jsdom": "^20.0.0", "less": "^4.1.1", - "lint-staged": "^10.0.0-1", "lodash": "^4.17.21", "miniprogram-api-typings": "^3.4.6", "miniprogram-automator": "^0.10.0", @@ -108,19 +101,6 @@ "vue": "^3.2.4", "vue-router": "^4.0.11" }, - "config": { - "commitizen": { - "path": "./node_modules/cz-conventional-changelog" - } - }, - "lint-staged": { - "{src,example,script}/**/*.{js,ts,wxml,html,json,less}": [ - "prettier --write" - ], - "{src,example}/**/*.{js,ts}": [ - "eslint --fix" - ] - }, "dependencies": { "dayjs": "^1.10.7", "tdesign-flutter": "file:" diff --git a/tdesign-site/pnpm-lock.yaml b/tdesign-site/pnpm-lock.yaml index 36ed9b6e2..ee1b9044d 100644 --- a/tdesign-site/pnpm-lock.yaml +++ b/tdesign-site/pnpm-lock.yaml @@ -27,12 +27,6 @@ importers: '@babel/preset-typescript': specifier: ^7.12.7 version: 7.28.5(@babel/core@7.28.5) - '@commitlint/cli': - specifier: ^16.0.2 - version: 16.3.0 - '@commitlint/config-conventional': - specifier: ^16.0.0 - version: 16.2.4 '@rollup/plugin-node-resolve': specifier: ^13.0.5 version: 13.3.0(rollup@2.77.3) @@ -63,15 +57,9 @@ importers: babel-jest: specifier: ^26.6.3 version: 26.6.3(@babel/core@7.28.5) - commitizen: - specifier: ^4.2.4 - version: 4.3.1(@types/node@25.0.3)(typescript@4.7.4) cross-env: specifier: ^7.0.2 version: 7.0.3 - cz-conventional-changelog: - specifier: ^3.3.0 - version: 3.3.0(@types/node@25.0.3)(typescript@4.7.4) del: specifier: ^6.1.1 version: 6.1.1 @@ -129,9 +117,6 @@ importers: gulp-typescript: specifier: ^6.0.0-alpha.1 version: 6.0.0-alpha.1(typescript@4.7.4) - husky: - specifier: ^7.0.4 - version: 7.0.4 jest: specifier: ^26.6.3 version: 26.6.3(ts-node@10.9.2(@types/node@25.0.3)(typescript@4.7.4)) @@ -144,9 +129,6 @@ importers: less: specifier: ^4.1.1 version: 4.5.1 - lint-staged: - specifier: ^10.0.0-1 - version: 10.5.4 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -820,95 +802,6 @@ packages: engines: {node: '>=0.1.95'} hasBin: true - '@commitlint/cli@16.3.0': - resolution: {integrity: sha512-P+kvONlfsuTMnxSwWE1H+ZcPMY3STFaHb2kAacsqoIkNx66O0T7sTpBxpxkMrFPyhkJiLJnJWMhk4bbvYD3BMA==} - engines: {node: '>=v12'} - hasBin: true - - '@commitlint/config-conventional@16.2.4': - resolution: {integrity: sha512-av2UQJa3CuE5P0dzxj/o/B9XVALqYzEViHrMXtDrW9iuflrqCStWBAioijppj9URyz6ONpohJKAtSdgAOE0gkA==} - engines: {node: '>=v12'} - - '@commitlint/config-validator@16.2.1': - resolution: {integrity: sha512-hogSe0WGg7CKmp4IfNbdNES3Rq3UEI4XRPB8JL4EPgo/ORq5nrGTVzxJh78omibNuB8Ho4501Czb1Er1MoDWpw==} - engines: {node: '>=v12'} - - '@commitlint/config-validator@20.2.0': - resolution: {integrity: sha512-SQCBGsL9MFk8utWNSthdxd9iOD1pIVZSHxGBwYIGfd67RTjxqzFOSAYeQVXOu3IxRC3YrTOH37ThnTLjUlyF2w==} - engines: {node: '>=v18'} - - '@commitlint/ensure@16.2.1': - resolution: {integrity: sha512-/h+lBTgf1r5fhbDNHOViLuej38i3rZqTQnBTk+xEg+ehOwQDXUuissQ5GsYXXqI5uGy+261ew++sT4EA3uBJ+A==} - engines: {node: '>=v12'} - - '@commitlint/execute-rule@16.2.1': - resolution: {integrity: sha512-oSls82fmUTLM6cl5V3epdVo4gHhbmBFvCvQGHBRdQ50H/690Uq1Dyd7hXMuKITCIdcnr9umyDkr8r5C6HZDF3g==} - engines: {node: '>=v12'} - - '@commitlint/execute-rule@20.0.0': - resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} - engines: {node: '>=v18'} - - '@commitlint/format@16.2.1': - resolution: {integrity: sha512-Yyio9bdHWmNDRlEJrxHKglamIk3d6hC0NkEUW6Ti6ipEh2g0BAhy8Od6t4vLhdZRa1I2n+gY13foy+tUgk0i1Q==} - engines: {node: '>=v12'} - - '@commitlint/is-ignored@16.2.4': - resolution: {integrity: sha512-Lxdq9aOAYCOOOjKi58ulbwK/oBiiKz+7Sq0+/SpFIEFwhHkIVugvDvWjh2VRBXmRC/x5lNcjDcYEwS/uYUvlYQ==} - engines: {node: '>=v12'} - - '@commitlint/lint@16.2.4': - resolution: {integrity: sha512-AUDuwOxb2eGqsXbTMON3imUGkc1jRdtXrbbohiLSCSk3jFVXgJLTMaEcr39pR00N8nE9uZ+V2sYaiILByZVmxQ==} - engines: {node: '>=v12'} - - '@commitlint/load@16.3.0': - resolution: {integrity: sha512-3tykjV/iwbkv2FU9DG+NZ/JqmP0Nm3b7aDwgCNQhhKV5P74JAuByULkafnhn+zsFGypG1qMtI5u+BZoa9APm0A==} - engines: {node: '>=v12'} - - '@commitlint/load@20.2.0': - resolution: {integrity: sha512-iAK2GaBM8sPFTSwtagI67HrLKHIUxQc2BgpgNc/UMNme6LfmtHpIxQoN1TbP+X1iz58jq32HL1GbrFTCzcMi6g==} - engines: {node: '>=v18'} - - '@commitlint/message@16.2.1': - resolution: {integrity: sha512-2eWX/47rftViYg7a3axYDdrgwKv32mxbycBJT6OQY/MJM7SUfYNYYvbMFOQFaA4xIVZt7t2Alyqslbl6blVwWw==} - engines: {node: '>=v12'} - - '@commitlint/parse@16.2.1': - resolution: {integrity: sha512-2NP2dDQNL378VZYioLrgGVZhWdnJO4nAxQl5LXwYb08nEcN+cgxHN1dJV8OLJ5uxlGJtDeR8UZZ1mnQ1gSAD/g==} - engines: {node: '>=v12'} - - '@commitlint/read@16.2.1': - resolution: {integrity: sha512-tViXGuaxLTrw2r7PiYMQOFA2fueZxnnt0lkOWqKyxT+n2XdEMGYcI9ID5ndJKXnfPGPppD0w/IItKsIXlZ+alw==} - engines: {node: '>=v12'} - - '@commitlint/resolve-extends@16.2.1': - resolution: {integrity: sha512-NbbCMPKTFf2J805kwfP9EO+vV+XvnaHRcBy6ud5dF35dxMsvdJqke54W3XazXF1ZAxC4a3LBy4i/GNVBAthsEg==} - engines: {node: '>=v12'} - - '@commitlint/resolve-extends@20.2.0': - resolution: {integrity: sha512-KVoLDi9BEuqeq+G0wRABn4azLRiCC22/YHR2aCquwx6bzCHAIN8hMt3Nuf1VFxq/c8ai6s8qBxE8+ZD4HeFTlQ==} - engines: {node: '>=v18'} - - '@commitlint/rules@16.2.4': - resolution: {integrity: sha512-rK5rNBIN2ZQNQK+I6trRPK3dWa0MtaTN4xnwOma1qxa4d5wQMQJtScwTZjTJeallFxhOgbNOgr48AMHkdounVg==} - engines: {node: '>=v12'} - - '@commitlint/to-lines@16.2.1': - resolution: {integrity: sha512-9/VjpYj5j1QeY3eiog1zQWY6axsdWAc0AonUUfyZ7B0MVcRI0R56YsHAfzF6uK/g/WwPZaoe4Lb1QCyDVnpVaQ==} - engines: {node: '>=v12'} - - '@commitlint/top-level@16.2.1': - resolution: {integrity: sha512-lS6GSieHW9y6ePL73ied71Z9bOKyK+Ib9hTkRsB8oZFAyQZcyRwq2w6nIa6Fngir1QW51oKzzaXfJL94qwImyw==} - engines: {node: '>=v12'} - - '@commitlint/types@16.2.1': - resolution: {integrity: sha512-7/z7pA7BM0i8XvMSBynO7xsB3mVQPUZbVn6zMIlp/a091XJ3qAXRXc+HwLYhiIdzzS5fuxxNIHZMGHVD4HJxdA==} - engines: {node: '>=v12'} - - '@commitlint/types@20.2.0': - resolution: {integrity: sha512-KTy0OqRDLR5y/zZMnizyx09z/rPlPC/zKhYgH8o/q6PuAjoQAKlRfY4zzv0M64yybQ//6//4H1n14pxaLZfUnA==} - engines: {node: '>=v18'} - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1286,9 +1179,6 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/conventional-commits-parser@5.0.2': - resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==} - '@types/estree@0.0.39': resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} @@ -1797,10 +1687,6 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - at-least-node@1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} - atob@2.1.2: resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} engines: {node: '>= 4.5.0'} @@ -1961,10 +1847,6 @@ packages: resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} engines: {node: '>=0.10.0'} - cachedir@2.3.0: - resolution: {integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==} - engines: {node: '>=6'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2019,10 +1901,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -2036,9 +1914,6 @@ packages: character-reference-invalid@1.1.4: resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} - chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} @@ -2071,10 +1946,6 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} - cli-truncate@2.1.0: - resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} - engines: {node: '>=8'} - cli-width@3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} @@ -2088,10 +1959,6 @@ packages: cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - clone-buffer@1.0.0: resolution: {integrity: sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==} engines: {node: '>= 0.10'} @@ -2155,9 +2022,6 @@ packages: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true - colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2165,19 +2029,10 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commander@6.2.1: - resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} - engines: {node: '>= 6'} - commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} - commitizen@4.3.1: - resolution: {integrity: sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw==} - engines: {node: '>= 12'} - hasBin: true - compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} @@ -2198,10 +2053,6 @@ packages: resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} engines: {node: '>=10'} - conventional-changelog-conventionalcommits@4.6.3: - resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==} - engines: {node: '>=10'} - conventional-changelog-core@4.2.4: resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==} engines: {node: '>=10'} @@ -2211,9 +2062,6 @@ packages: engines: {node: '>=10'} hasBin: true - conventional-commit-types@3.0.0: - resolution: {integrity: sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==} - conventional-commits-filter@2.0.7: resolution: {integrity: sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==} engines: {node: '>=10'} @@ -2249,35 +2097,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cosmiconfig-typescript-loader@2.0.2: - resolution: {integrity: sha512-KmE+bMjWMXJbkWCeY4FJX/npHuZPNr9XF9q9CIQ/bpFwi1qHfCmSiKarrCcRa0LO4fWjk93pVoeRtJAkTGcYNw==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@types/node': '*' - cosmiconfig: '>=7' - typescript: '>=3' - - cosmiconfig-typescript-loader@6.2.0: - resolution: {integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==} - engines: {node: '>=v18'} - peerDependencies: - '@types/node': '*' - cosmiconfig: '>=9' - typescript: '>=5' - cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} - cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -2330,10 +2153,6 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - cz-conventional-changelog@3.3.0: - resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==} - engines: {node: '>= 10'} - d@1.0.2: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} @@ -2418,9 +2237,6 @@ packages: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} - dedent@0.7.0: - resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2471,10 +2287,6 @@ packages: resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} engines: {node: '>=0.10.0'} - detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - detect-newline@2.1.0: resolution: {integrity: sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==} engines: {node: '>=0.10.0'} @@ -2631,10 +2443,6 @@ packages: resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} engines: {node: '>=0.12'} - env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - errno@0.1.8: resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} hasBin: true @@ -2976,10 +2784,6 @@ packages: resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} engines: {node: '>=10'} - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - execall@2.0.0: resolution: {integrity: sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==} engines: {node: '>=8'} @@ -3024,10 +2828,6 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} - extglob@2.0.4: resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} engines: {node: '>=0.10.0'} @@ -3094,12 +2894,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - find-node-modules@2.1.3: - resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==} - - find-root@1.1.0: - resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} - find-up@1.1.2: resolution: {integrity: sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==} engines: {node: '>=0.10.0'} @@ -3112,10 +2906,6 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - findup-sync@2.0.0: resolution: {integrity: sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==} engines: {node: '>= 0.10'} @@ -3124,10 +2914,6 @@ packages: resolution: {integrity: sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==} engines: {node: '>= 0.10'} - findup-sync@4.0.0: - resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} - engines: {node: '>= 8'} - fined@1.2.0: resolution: {integrity: sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==} engines: {node: '>= 0.10'} @@ -3190,14 +2976,6 @@ packages: resolution: {integrity: sha512-05cXDIwNbFaoFWaz5gNHlUTbH5whiss/hr/ibzPd4MH3cR4w0ZKeIPiVdbyJurg3O5r/Bjpvn9KOb1/rPMf3nA==} engines: {node: '>=0.10.0'} - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - - fs-extra@9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} - fs-mkdirp-stream@1.0.0: resolution: {integrity: sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==} engines: {node: '>= 0.10'} @@ -3257,9 +3035,6 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} - get-own-enumerable-property-symbols@3.0.2: - resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} - get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -3289,10 +3064,6 @@ packages: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -3342,14 +3113,6 @@ packages: engines: {node: '>=12'} deprecated: Glob versions prior to v9 are no longer supported - global-directory@4.0.1: - resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} - engines: {node: '>=18'} - - global-dirs@0.1.1: - resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} - engines: {node: '>=4'} - global-modules@1.0.0: resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} engines: {node: '>=0.10.0'} @@ -3582,15 +3345,6 @@ packages: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - - husky@7.0.4: - resolution: {integrity: sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==} - engines: {node: '>=12'} - hasBin: true - hybrids@7.1.0: resolution: {integrity: sha512-ZPguGrSfulNM92+e2MyvtFdCDEW4gwi0atHr+GLrD4vc+kLWbYkBdVvYSKXNwMEa1WNfO/6fcegZSd2FHuC/kQ==} @@ -3640,9 +3394,6 @@ packages: engines: {node: '>=8'} hasBin: true - import-meta-resolve@4.2.0: - resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} - import-regex@1.1.0: resolution: {integrity: sha512-EblpleIyIdATUKj8ovFojUHyToxgjeKXQgTHZBGZ4cEkbtV21BlO1PSrzZQ6Fei2fgk7uhDeEx656yvPhlRGeA==} engines: {node: '>=0.10.0'} @@ -3668,14 +3419,6 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - ini@4.1.1: - resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - inquirer@8.2.5: - resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} - engines: {node: '>=12.0.0'} - inquirer@8.2.7: resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==} engines: {node: '>=12.0.0'} @@ -3861,10 +3604,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-obj@1.0.1: - resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} - engines: {node: '>=0.10.0'} - is-obj@2.0.0: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} @@ -3903,10 +3642,6 @@ packages: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} - is-regexp@1.0.0: - resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} - engines: {node: '>=0.10.0'} - is-regexp@2.1.0: resolution: {integrity: sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==} engines: {node: '>=6'} @@ -4194,10 +3929,6 @@ packages: jimp@0.6.8: resolution: {integrity: sha512-F7emeG7Hp61IM8VFbNvWENLTuHe0ghizWPuP4JS9ujx2r5mCVYEd/zdaz6M2M42ZdN41blxPajLWl9FXo7Mr2Q==} - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} - hasBin: true - jpeg-js@0.3.7: resolution: {integrity: sha512-9IXdWudL61npZjvLuVe/ktHiA41iE8qFyLB+4VDTblEsWBzeg8WQTlktdUK4CdncUqtUgUg0bbOmTE2bKBKaBQ==} @@ -4265,9 +3996,6 @@ packages: engines: {node: '>=6'} hasBin: true - jsonfile@6.2.0: - resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} @@ -4356,19 +4084,6 @@ packages: linkify-it@3.0.3: resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} - lint-staged@10.5.4: - resolution: {integrity: sha512-EechC3DdFic/TdOPgj/RB3FicqE6932LTHCUm0Y2fsD9KGlLB+RwJl2q1IYBIvEsKzDOgn0D4gll+YxG5RsrKg==} - hasBin: true - - listr2@3.14.0: - resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==} - engines: {node: '>=10.0.0'} - peerDependencies: - enquirer: '>= 2.3.0 < 3' - peerDependenciesMeta: - enquirer: - optional: true - load-bmfont@1.4.2: resolution: {integrity: sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==} @@ -4392,34 +4107,18 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} lodash.ismatch@4.4.0: resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - - lodash.map@4.6.0: - resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.mergewith@4.6.2: - resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - lodash.truncate@4.4.2: resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} - lodash.uniq@4.5.0: - resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -4427,17 +4126,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} - log-update@4.0.0: - resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} - engines: {node: '>=10'} - longest-streak@2.0.4: resolution: {integrity: sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==} - longest@2.0.1: - resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==} - engines: {node: '>=0.10.0'} - lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -4563,9 +4254,6 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - merge@2.1.1: - resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} - micromark@2.11.4: resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} @@ -4615,9 +4303,6 @@ packages: minimist@0.0.8: resolution: {integrity: sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==} - minimist@1.2.7: - resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -4868,10 +4553,6 @@ packages: resolution: {integrity: sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==} engines: {node: '>=0.10.0'} - os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -4892,10 +4573,6 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - p-locate@2.0.0: resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} engines: {node: '>=4'} @@ -4904,10 +4581,6 @@ packages: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} @@ -5092,9 +4765,6 @@ packages: engines: {node: '>=18'} hasBin: true - please-upgrade-node@3.2.0: - resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} - plugin-error@0.1.2: resolution: {integrity: sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==} engines: {node: '>=0.10.0'} @@ -5472,10 +5142,6 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve-global@1.0.0: - resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==} - engines: {node: '>=8'} - resolve-options@1.1.0: resolution: {integrity: sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==} engines: {node: '>= 0.10'} @@ -5501,9 +5167,6 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfdc@1.4.1: - resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -5573,9 +5236,6 @@ packages: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} - semver-compare@1.0.0: - resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} - semver-greatest-satisfied-range@1.1.0: resolution: {integrity: sha512-Ny/iyOzSSa8M5ML46IAx3iXc6tfOsYU2R4AXi2UpHk60Zrgyq6eqPj/xiOfS0rRl/iiQ/rdJkVjw/5cdUyCntQ==} engines: {node: '>= 0.10'} @@ -5588,11 +5248,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.3.7: - resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -5666,10 +5321,6 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - slice-ansi@3.0.0: - resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} - engines: {node: '>=8'} - slice-ansi@4.0.0: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} @@ -5782,10 +5433,6 @@ packages: streamx@2.23.0: resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} - string-argv@0.3.1: - resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} - engines: {node: '>=0.6.19'} - string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -5823,10 +5470,6 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - stringify-object@3.3.0: - resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} - engines: {node: '>=4'} - strip-ansi@3.0.1: resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} engines: {node: '>=0.10.0'} @@ -5989,10 +5632,6 @@ packages: tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -6210,10 +5849,6 @@ packages: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - unorm@1.6.0: resolution: {integrity: sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==} engines: {node: '>= 0.4.0'} @@ -6548,10 +6183,6 @@ packages: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - yargs-parser@22.0.0: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} @@ -6567,10 +6198,6 @@ packages: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - yargs@7.1.2: resolution: {integrity: sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==} @@ -6578,10 +6205,6 @@ packages: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - zwitch@1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} @@ -7375,158 +6998,10 @@ snapshots: exec-sh: 0.3.6 minimist: 1.2.8 - '@commitlint/cli@16.3.0': - dependencies: - '@commitlint/format': 16.2.1 - '@commitlint/lint': 16.2.4 - '@commitlint/load': 16.3.0 - '@commitlint/read': 16.2.1 - '@commitlint/types': 16.2.1 - lodash: 4.17.21 - resolve-from: 5.0.0 - resolve-global: 1.0.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - - '@commitlint/config-conventional@16.2.4': - dependencies: - conventional-changelog-conventionalcommits: 4.6.3 - - '@commitlint/config-validator@16.2.1': - dependencies: - '@commitlint/types': 16.2.1 - ajv: 6.12.6 - - '@commitlint/config-validator@20.2.0': - dependencies: - '@commitlint/types': 20.2.0 - ajv: 8.17.1 - optional: true - - '@commitlint/ensure@16.2.1': - dependencies: - '@commitlint/types': 16.2.1 - lodash: 4.17.21 - - '@commitlint/execute-rule@16.2.1': {} - - '@commitlint/execute-rule@20.0.0': - optional: true - - '@commitlint/format@16.2.1': - dependencies: - '@commitlint/types': 16.2.1 - chalk: 4.1.2 - - '@commitlint/is-ignored@16.2.4': - dependencies: - '@commitlint/types': 16.2.1 - semver: 7.3.7 - - '@commitlint/lint@16.2.4': - dependencies: - '@commitlint/is-ignored': 16.2.4 - '@commitlint/parse': 16.2.1 - '@commitlint/rules': 16.2.4 - '@commitlint/types': 16.2.1 - - '@commitlint/load@16.3.0': - dependencies: - '@commitlint/config-validator': 16.2.1 - '@commitlint/execute-rule': 16.2.1 - '@commitlint/resolve-extends': 16.2.1 - '@commitlint/types': 16.2.1 - '@types/node': 25.0.3 - chalk: 4.1.2 - cosmiconfig: 7.1.0 - cosmiconfig-typescript-loader: 2.0.2(@types/node@25.0.3)(cosmiconfig@7.1.0)(typescript@4.7.4) - lodash: 4.17.21 - resolve-from: 5.0.0 - typescript: 4.7.4 - transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - - '@commitlint/load@20.2.0(@types/node@25.0.3)(typescript@4.7.4)': - dependencies: - '@commitlint/config-validator': 20.2.0 - '@commitlint/execute-rule': 20.0.0 - '@commitlint/resolve-extends': 20.2.0 - '@commitlint/types': 20.2.0 - chalk: 5.6.2 - cosmiconfig: 9.0.0(typescript@4.7.4) - cosmiconfig-typescript-loader: 6.2.0(@types/node@25.0.3)(cosmiconfig@9.0.0(typescript@4.7.4))(typescript@4.7.4) - lodash.isplainobject: 4.0.6 - lodash.merge: 4.6.2 - lodash.uniq: 4.5.0 - transitivePeerDependencies: - - '@types/node' - - typescript - optional: true - - '@commitlint/message@16.2.1': {} - - '@commitlint/parse@16.2.1': - dependencies: - '@commitlint/types': 16.2.1 - conventional-changelog-angular: 5.0.13 - conventional-commits-parser: 3.2.4 - - '@commitlint/read@16.2.1': - dependencies: - '@commitlint/top-level': 16.2.1 - '@commitlint/types': 16.2.1 - fs-extra: 10.1.0 - git-raw-commits: 2.0.11 - - '@commitlint/resolve-extends@16.2.1': - dependencies: - '@commitlint/config-validator': 16.2.1 - '@commitlint/types': 16.2.1 - import-fresh: 3.3.1 - lodash: 4.17.21 - resolve-from: 5.0.0 - resolve-global: 1.0.0 - - '@commitlint/resolve-extends@20.2.0': - dependencies: - '@commitlint/config-validator': 20.2.0 - '@commitlint/types': 20.2.0 - global-directory: 4.0.1 - import-meta-resolve: 4.2.0 - lodash.mergewith: 4.6.2 - resolve-from: 5.0.0 - optional: true - - '@commitlint/rules@16.2.4': - dependencies: - '@commitlint/ensure': 16.2.1 - '@commitlint/message': 16.2.1 - '@commitlint/to-lines': 16.2.1 - '@commitlint/types': 16.2.1 - execa: 5.1.1 - - '@commitlint/to-lines@16.2.1': {} - - '@commitlint/top-level@16.2.1': - dependencies: - find-up: 5.0.0 - - '@commitlint/types@16.2.1': - dependencies: - chalk: 4.1.2 - - '@commitlint/types@20.2.0': - dependencies: - '@types/conventional-commits-parser': 5.0.2 - chalk: 5.6.2 - optional: true - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 + optional: true '@esbuild/linux-loong64@0.14.54': optional: true @@ -8017,6 +7492,7 @@ snapshots: dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + optional: true '@nodelib/fs.scandir@2.1.5': dependencies: @@ -8096,13 +7572,17 @@ snapshots: '@tootallnate/once@2.0.0': {} - '@tsconfig/node10@1.0.12': {} + '@tsconfig/node10@1.0.12': + optional: true - '@tsconfig/node12@1.0.11': {} + '@tsconfig/node12@1.0.11': + optional: true - '@tsconfig/node14@1.0.3': {} + '@tsconfig/node14@1.0.3': + optional: true - '@tsconfig/node16@1.0.4': {} + '@tsconfig/node16@1.0.4': + optional: true '@types/babel__core@7.20.5': dependencies: @@ -8125,11 +7605,6 @@ snapshots: dependencies: '@babel/types': 7.28.5 - '@types/conventional-commits-parser@5.0.2': - dependencies: - '@types/node': 25.0.3 - optional: true - '@types/estree@0.0.39': {} '@types/expect@1.20.4': {} @@ -8567,7 +8042,8 @@ snapshots: archy@1.0.0: {} - arg@4.1.3: {} + arg@4.1.3: + optional: true argparse@1.0.10: dependencies: @@ -8715,8 +8191,6 @@ snapshots: asynckit@0.4.0: {} - at-least-node@1.0.0: {} - atob@2.1.2: {} autoprefixer@9.8.8: @@ -8941,8 +8415,6 @@ snapshots: union-value: 1.0.1 unset-value: 1.0.0 - cachedir@2.3.0: {} - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -9005,9 +8477,6 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.6.2: - optional: true - char-regex@1.0.2: {} character-entities-legacy@1.1.4: {} @@ -9016,8 +8485,6 @@ snapshots: character-reference-invalid@1.1.4: {} - chardet@0.7.0: {} - chardet@2.1.1: {} chokidar@2.1.8: @@ -9059,11 +8526,6 @@ snapshots: cli-spinners@2.9.2: {} - cli-truncate@2.1.0: - dependencies: - slice-ansi: 3.0.0 - string-width: 4.2.3 - cli-width@3.0.0: {} cliui@3.2.0: @@ -9084,12 +8546,6 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - clone-buffer@1.0.0: {} clone-regexp@2.2.0: @@ -9142,38 +8598,14 @@ snapshots: color-support@1.1.3: {} - colorette@2.0.20: {} - combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 commander@2.20.3: {} - commander@6.2.1: {} - commander@7.2.0: {} - commitizen@4.3.1(@types/node@25.0.3)(typescript@4.7.4): - dependencies: - cachedir: 2.3.0 - cz-conventional-changelog: 3.3.0(@types/node@25.0.3)(typescript@4.7.4) - dedent: 0.7.0 - detect-indent: 6.1.0 - find-node-modules: 2.1.3 - find-root: 1.1.0 - fs-extra: 9.1.0 - glob: 7.2.3 - inquirer: 8.2.5 - is-utf8: 0.2.1 - lodash: 4.17.21 - minimist: 1.2.7 - strip-bom: 4.0.0 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - '@types/node' - - typescript - compare-func@2.0.0: dependencies: array-ify: 1.0.0 @@ -9197,12 +8629,6 @@ snapshots: compare-func: 2.0.0 q: 1.5.1 - conventional-changelog-conventionalcommits@4.6.3: - dependencies: - compare-func: 2.0.0 - lodash: 4.17.21 - q: 1.5.1 - conventional-changelog-core@4.2.4: dependencies: add-stream: 1.0.0 @@ -9232,8 +8658,6 @@ snapshots: split: 1.0.1 through2: 4.0.2 - conventional-commit-types@3.0.0: {} - conventional-commits-filter@2.0.7: dependencies: lodash.ismatch: 4.4.0 @@ -9271,24 +8695,6 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@2.0.2(@types/node@25.0.3)(cosmiconfig@7.1.0)(typescript@4.7.4): - dependencies: - '@types/node': 25.0.3 - cosmiconfig: 7.1.0 - ts-node: 10.9.2(@types/node@25.0.3)(typescript@4.7.4) - typescript: 4.7.4 - transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - - cosmiconfig-typescript-loader@6.2.0(@types/node@25.0.3)(cosmiconfig@9.0.0(typescript@4.7.4))(typescript@4.7.4): - dependencies: - '@types/node': 25.0.3 - cosmiconfig: 9.0.0(typescript@4.7.4) - jiti: 2.6.1 - typescript: 4.7.4 - optional: true - cosmiconfig@7.1.0: dependencies: '@types/parse-json': 4.0.2 @@ -9297,18 +8703,9 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - cosmiconfig@9.0.0(typescript@4.7.4): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - parse-json: 5.2.0 - optionalDependencies: - typescript: 4.7.4 + create-require@1.1.1: optional: true - create-require@1.1.1: {} - cross-env@7.0.3: dependencies: cross-spawn: 7.0.6 @@ -9361,20 +8758,6 @@ snapshots: csstype@3.2.3: {} - cz-conventional-changelog@3.3.0(@types/node@25.0.3)(typescript@4.7.4): - dependencies: - chalk: 2.4.2 - commitizen: 4.3.1(@types/node@25.0.3)(typescript@4.7.4) - conventional-commit-types: 3.0.0 - lodash.map: 4.6.0 - longest: 2.0.1 - word-wrap: 1.2.5 - optionalDependencies: - '@commitlint/load': 20.2.0(@types/node@25.0.3)(typescript@4.7.4) - transitivePeerDependencies: - - '@types/node' - - typescript - d@1.0.2: dependencies: es5-ext: 0.10.64 @@ -9451,8 +8834,6 @@ snapshots: decode-uri-component@0.2.2: {} - dedent@0.7.0: {} - deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -9507,8 +8888,6 @@ snapshots: detect-file@1.0.0: {} - detect-indent@6.1.0: {} - detect-newline@2.1.0: {} detect-newline@3.1.0: {} @@ -9578,7 +8957,8 @@ snapshots: diff-sequences@27.5.1: {} - diff@4.0.2: {} + diff@4.0.2: + optional: true dijkstrajs@1.0.3: {} @@ -9676,9 +9056,6 @@ snapshots: entities@7.0.0: {} - env-paths@2.2.1: - optional: true - errno@0.1.8: dependencies: prr: 1.0.1 @@ -10095,18 +9472,6 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - execall@2.0.0: dependencies: clone-regexp: 2.2.0 @@ -10161,12 +9526,6 @@ snapshots: extend@3.0.2: {} - external-editor@3.1.0: - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 - extglob@2.0.4: dependencies: array-unique: 0.3.2 @@ -10243,13 +9602,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - find-node-modules@2.1.3: - dependencies: - findup-sync: 4.0.0 - merge: 2.1.1 - - find-root@1.1.0: {} - find-up@1.1.2: dependencies: path-exists: 2.1.0 @@ -10264,11 +9616,6 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - findup-sync@2.0.0: dependencies: detect-file: 1.0.0 @@ -10287,13 +9634,6 @@ snapshots: transitivePeerDependencies: - supports-color - findup-sync@4.0.0: - dependencies: - detect-file: 1.0.0 - is-glob: 4.0.3 - micromatch: 4.0.8 - resolve-dir: 1.0.1 - fined@1.2.0: dependencies: expand-tilde: 2.0.2 @@ -10359,19 +9699,6 @@ snapshots: dependencies: null-check: 1.0.0 - fs-extra@10.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - - fs-extra@9.1.0: - dependencies: - at-least-node: 1.0.0 - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - fs-mkdirp-stream@1.0.0: dependencies: graceful-fs: 4.2.11 @@ -10432,8 +9759,6 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 - get-own-enumerable-property-symbols@3.0.2: {} - get-package-type@0.1.0: {} get-pkg-repo@4.2.1: @@ -10460,8 +9785,6 @@ snapshots: dependencies: pump: 3.0.3 - get-stream@6.0.1: {} - get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -10543,15 +9866,6 @@ snapshots: minimatch: 5.1.6 once: 1.4.0 - global-directory@4.0.1: - dependencies: - ini: 4.1.1 - optional: true - - global-dirs@0.1.1: - dependencies: - ini: 1.3.8 - global-modules@1.0.0: dependencies: global-prefix: 1.0.2 @@ -10877,10 +10191,6 @@ snapshots: human-signals@1.1.1: {} - human-signals@2.1.0: {} - - husky@7.0.4: {} - hybrids@7.1.0: {} hybrids@8.2.2: {} @@ -10918,9 +10228,6 @@ snapshots: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 - import-meta-resolve@4.2.0: - optional: true - import-regex@1.1.0: {} imurmurhash@0.1.4: {} @@ -10938,27 +10245,6 @@ snapshots: ini@1.3.8: {} - ini@4.1.1: - optional: true - - inquirer@8.2.5: - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.8.2 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 7.0.0 - inquirer@8.2.7(@types/node@25.0.3): dependencies: '@inquirer/external-editor': 1.0.3(@types/node@25.0.3) @@ -11145,8 +10431,6 @@ snapshots: is-number@7.0.0: {} - is-obj@1.0.1: {} - is-obj@2.0.0: {} is-path-cwd@2.2.0: {} @@ -11174,8 +10458,6 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - is-regexp@1.0.0: {} - is-regexp@2.1.0: {} is-relative@1.0.0: @@ -11349,7 +10631,7 @@ snapshots: jest-environment-jsdom: 26.6.2 jest-environment-node: 26.6.2 jest-get-type: 26.3.0 - jest-jasmine2: 26.6.3 + jest-jasmine2: 26.6.3(ts-node@10.9.2(@types/node@25.0.3)(typescript@4.7.4)) jest-regex-util: 26.0.0 jest-resolve: 26.6.2 jest-util: 26.6.2 @@ -11449,7 +10731,7 @@ snapshots: typescript: 4.7.4 xmlbuilder: 15.0.0 - jest-jasmine2@26.6.3: + jest-jasmine2@26.6.3(ts-node@10.9.2(@types/node@25.0.3)(typescript@4.7.4)): dependencies: '@babel/traverse': 7.28.5 '@jest/environment': 26.6.2 @@ -11470,7 +10752,11 @@ snapshots: pretty-format: 26.6.2 throat: 5.0.0 transitivePeerDependencies: + - bufferutil + - canvas - supports-color + - ts-node + - utf-8-validate jest-leak-detector@26.6.2: dependencies: @@ -11701,9 +10987,6 @@ snapshots: transitivePeerDependencies: - debug - jiti@2.6.1: - optional: true - jpeg-js@0.3.7: {} js-tokens@4.0.0: {} @@ -11807,12 +11090,6 @@ snapshots: json5@2.2.3: {} - jsonfile@6.2.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - jsonparse@1.3.1: {} just-debounce@1.1.0: {} @@ -11918,39 +11195,6 @@ snapshots: dependencies: uc.micro: 1.0.6 - lint-staged@10.5.4: - dependencies: - chalk: 4.1.2 - cli-truncate: 2.1.0 - commander: 6.2.1 - cosmiconfig: 7.1.0 - debug: 4.4.3 - dedent: 0.7.0 - enquirer: 2.4.1 - execa: 4.1.0 - listr2: 3.14.0(enquirer@2.4.1) - log-symbols: 4.1.0 - micromatch: 4.0.8 - normalize-path: 3.0.0 - please-upgrade-node: 3.2.0 - string-argv: 0.3.1 - stringify-object: 3.3.0 - transitivePeerDependencies: - - supports-color - - listr2@3.14.0(enquirer@2.4.1): - dependencies: - cli-truncate: 2.1.0 - colorette: 2.0.20 - log-update: 4.0.0 - p-map: 4.0.0 - rfdc: 1.4.1 - rxjs: 7.8.2 - through: 2.3.8 - wrap-ansi: 7.0.0 - optionalDependencies: - enquirer: 2.4.1 - load-bmfont@1.4.2(debug@4.4.3): dependencies: buffer-equal: 0.0.1 @@ -11995,29 +11239,14 @@ snapshots: dependencies: p-locate: 4.1.0 - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - lodash.debounce@4.0.8: {} lodash.ismatch@4.4.0: {} - lodash.isplainobject@4.0.6: - optional: true - - lodash.map@4.6.0: {} - lodash.merge@4.6.2: {} - lodash.mergewith@4.6.2: - optional: true - lodash.truncate@4.4.2: {} - lodash.uniq@4.5.0: - optional: true - lodash@4.17.21: {} log-symbols@4.1.0: @@ -12025,17 +11254,8 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 - log-update@4.0.0: - dependencies: - ansi-escapes: 4.3.2 - cli-cursor: 3.1.0 - slice-ansi: 4.0.0 - wrap-ansi: 6.2.0 - longest-streak@2.0.4: {} - longest@2.0.1: {} - lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -12066,7 +11286,8 @@ snapshots: dependencies: semver: 7.7.3 - make-error@1.3.6: {} + make-error@1.3.6: + optional: true make-iterator@1.0.1: dependencies: @@ -12193,8 +11414,6 @@ snapshots: merge2@1.4.1: {} - merge@2.1.1: {} - micromark@2.11.4: dependencies: debug: 4.4.3 @@ -12257,8 +11476,6 @@ snapshots: minimist@0.0.8: {} - minimist@1.2.7: {} - minimist@1.2.8: {} miniprogram-api-typings@3.12.3: {} @@ -12562,8 +11779,6 @@ snapshots: dependencies: lcid: 1.0.0 - os-tmpdir@1.0.2: {} - own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -12582,10 +11797,6 @@ snapshots: dependencies: p-try: 2.2.0 - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - p-locate@2.0.0: dependencies: p-limit: 1.3.0 @@ -12594,10 +11805,6 @@ snapshots: dependencies: p-limit: 2.3.0 - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - p-map@4.0.0: dependencies: aggregate-error: 3.1.0 @@ -12749,10 +11956,6 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - please-upgrade-node@3.2.0: - dependencies: - semver-compare: 1.0.0 - plugin-error@0.1.2: dependencies: ansi-cyan: 0.1.1 @@ -13164,10 +12367,6 @@ snapshots: resolve-from@5.0.0: {} - resolve-global@1.0.0: - dependencies: - global-dirs: 0.1.1 - resolve-options@1.1.0: dependencies: value-or-function: 3.0.0 @@ -13189,8 +12388,6 @@ snapshots: reusify@1.1.0: {} - rfdc@1.4.1: {} - rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -13269,8 +12466,6 @@ snapshots: extend-shallow: 2.0.1 kind-of: 6.0.3 - semver-compare@1.0.0: {} - semver-greatest-satisfied-range@1.1.0: dependencies: sver-compat: 1.5.0 @@ -13279,10 +12474,6 @@ snapshots: semver@6.3.1: {} - semver@7.3.7: - dependencies: - lru-cache: 6.0.0 - semver@7.7.3: {} set-blocking@2.0.0: {} @@ -13367,12 +12558,6 @@ snapshots: slash@3.0.0: {} - slice-ansi@3.0.0: - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - slice-ansi@4.0.0: dependencies: ansi-styles: 4.3.0 @@ -13507,8 +12692,6 @@ snapshots: - bare-abort-controller - react-native-b4a - string-argv@0.3.1: {} - string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -13566,12 +12749,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - stringify-object@3.3.0: - dependencies: - get-own-enumerable-property-symbols: 3.0.2 - is-obj: 1.0.1 - is-regexp: 1.0.0 - strip-ansi@3.0.1: dependencies: ansi-regex: 2.1.1 @@ -13786,10 +12963,6 @@ snapshots: tinycolor2@1.6.0: {} - tmp@0.0.33: - dependencies: - os-tmpdir: 1.0.2 - tmpl@1.0.5: {} to-absolute-glob@2.0.2: @@ -13861,6 +13034,7 @@ snapshots: typescript: 4.7.4 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optional: true tsconfig-paths@3.15.0: dependencies: @@ -14026,8 +13200,6 @@ snapshots: universalify@0.2.0: {} - universalify@2.0.1: {} - unorm@1.6.0: {} unset-value@1.0.0: @@ -14075,7 +13247,8 @@ snapshots: uuid@8.3.2: optional: true - v8-compile-cache-lib@3.0.1: {} + v8-compile-cache-lib@3.0.1: + optional: true v8-compile-cache@2.4.0: {} @@ -14382,8 +13555,6 @@ snapshots: yargs-parser@20.2.9: {} - yargs-parser@21.1.1: {} - yargs-parser@22.0.0: {} yargs-parser@5.0.1: @@ -14415,16 +13586,6 @@ snapshots: y18n: 5.0.8 yargs-parser: 20.2.9 - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - yargs@7.1.2: dependencies: camelcase: 3.0.0 @@ -14441,8 +13602,7 @@ snapshots: y18n: 3.2.2 yargs-parser: 5.0.1 - yn@3.1.1: {} - - yocto-queue@0.1.0: {} + yn@3.1.1: + optional: true zwitch@1.0.5: {} diff --git a/tdesign-site/src/action-sheet/README.md b/tdesign-site/src/action-sheet/README.md index 79a1a954c..2c8614a1b 100644 --- a/tdesign-site/src/action-sheet/README.md +++ b/tdesign-site/src/action-sheet/README.md @@ -897,14 +897,12 @@ Widget _buildIconListLeftActionSheet(BuildContext context) { | badge | TBadge? | - | 角标 | | description | String? | - | 描述信息 | | disabled | bool | false | 是否禁用 | -| group | String? | - | 分组,用于带描述多行滚动宫格 | +| group | String? | - | 分组,用于带描述多行滚动宫格 当[TActionSheet.theme]等于[TActionSheetTheme.group]时有效 有效时,如果该值未配置整个[TActionSheetItem]会被忽略,即不会展示 | | icon | Widget? | - | 图标 | | iconSize | double? | - | 图标大小 | | label | String | - | 标题 | | textStyle | TextStyle? | - | 标题样式 | -``` -``` ### TActionSheet #### 简介 @@ -913,23 +911,23 @@ Widget _buildIconListLeftActionSheet(BuildContext context) { | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | +| context | BuildContext | - | 上下文 | | align | TActionSheetAlign | TActionSheetAlign.center | 对齐方式 | | cancelText | String? | - | 取消按钮的文本 | | closeOnOverlayClick | bool | true | 点击蒙层时是否关闭 | -| context | BuildContext | context | 上下文 | -| count | int | 8 | 每页显示的项目数 | -| description | String? | - | 描述文本 | -| itemHeight | double | 96.0 | 项目的行高 | -| itemMinWidth | double | 80.0 | 项目的最小宽度 | +| count | int | 8 | 每页显示的项目数 当[theme]等于[TActionSheetTheme.grid]且[showPagination]为true时有效 | +| description | String? | - | 描述文本 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.list]时有效 | +| itemHeight | double | 96.0 | 项目的行高 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.group]时有效 | +| itemMinWidth | double | 80.0 | 项目的最小宽度 当[theme]等于[TActionSheetTheme.grid]且[scrollable]为true时有效 或当[theme]等于[TActionSheetTheme.group]时有效 | | items | List | - | ActionSheet的项目列表 | | onCancel | VoidCallback? | - | 取消按钮的回调函数 | | onClose | VoidCallback? | - | 关闭时的回调函数 | | onSelected | TActionSheetItemCallback? | - | 选择项目时的回调函数 | -| rows | int | 2 | 显示的行数 | -| scrollable | bool | false | 是否可以横向滚动 | +| rows | int | 2 | 显示的行数 当[theme]等于[TActionSheetTheme.grid]时有效 | +| scrollable | bool | false | 是否可以横向滚动 当[theme]等于[TActionSheetTheme.grid]且[showPagination]为false时有效 | | showCancel | bool | true | 是否显示取消按钮 | | showOverlay | bool | true | 是否显示遮罩层 | -| showPagination | bool | false | 是否显示分页 | +| showPagination | bool | false | 是否显示分页 当[theme]等于[TActionSheetTheme.grid]时有效 | | theme | TActionSheetTheme | TActionSheetTheme.list | 主题样式 | | useSafeArea | bool | true | 使用安全区域 | | visible | bool | false | 是否立即显示 | @@ -937,11 +935,106 @@ Widget _buildIconListLeftActionSheet(BuildContext context) { #### 静态方法 -| 名称 | 返回类型 | 参数 | 说明 | +##### TActionSheet.showGridActionSheet + +显示宫格类型面板 + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | 上下文 | +| items | List | - | ActionSheet的项目列表 | +| align | TActionSheetAlign | TActionSheetAlign.center | 对齐方式 | +| cancelText | String? | - | 取消按钮的文本 | +| showCancel | bool | true | 是否显示取消按钮 | +| onSelected | TActionSheetItemCallback? | - | 选择项目时的回调函数 | +| showOverlay | bool | true | 是否显示遮罩层 | +| closeOnOverlayClick | bool | true | 点击蒙层时是否关闭 | +| count | int | 8 | 每页显示的项目数 当[theme]等于[TActionSheetTheme.grid]且[showPagination]为true时有效 | +| rows | int | 2 | 显示的行数 当[theme]等于[TActionSheetTheme.grid]时有效 | +| itemHeight | double | 96.0 | 项目的行高 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.group]时有效 | +| itemMinWidth | double | 80.0 | 项目的最小宽度 当[theme]等于[TActionSheetTheme.grid]且[scrollable]为true时有效 或当[theme]等于[TActionSheetTheme.group]时有效 | +| scrollable | bool | false | 是否可以横向滚动 当[theme]等于[TActionSheetTheme.grid]且[showPagination]为false时有效 | +| showPagination | bool | false | 是否显示分页 当[theme]等于[TActionSheetTheme.grid]时有效 | +| onCancel | VoidCallback? | - | 取消按钮的回调函数 | +| description | String? | - | 描述文本 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.list]时有效 | +| onClose | VoidCallback? | - | 关闭时的回调函数 | +| useSafeArea | bool | true | 使用安全区域 | + + +##### TActionSheet.showGroupActionSheet + +显示分组类型面板 + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | 上下文 | +| items | List | - | ActionSheet的项目列表 | +| align | TActionSheetAlign | TActionSheetAlign.left | 对齐方式 | +| cancelText | String? | - | 取消按钮的文本 | +| showCancel | bool | true | 是否显示取消按钮 | +| onSelected | TActionSheetItemCallback? | - | 选择项目时的回调函数 | +| showOverlay | bool | true | 是否显示遮罩层 | +| closeOnOverlayClick | bool | true | 点击蒙层时是否关闭 | +| itemHeight | double | 96.0 | 项目的行高 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.group]时有效 | +| itemMinWidth | double | 80.0 | 项目的最小宽度 当[theme]等于[TActionSheetTheme.grid]且[scrollable]为true时有效 或当[theme]等于[TActionSheetTheme.group]时有效 | +| onCancel | VoidCallback? | - | 取消按钮的回调函数 | +| onClose | VoidCallback? | - | 关闭时的回调函数 | +| useSafeArea | bool | true | 使用安全区域 | + + +##### TActionSheet.showListActionSheet + +显示列表类型面板 + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| showGridActionSheet | | required BuildContext context, required List items, TActionSheetAlign align, String? cancelText, bool showCancel, TActionSheetItemCallback? onSelected, bool showOverlay, bool closeOnOverlayClick, int count, int rows, double itemHeight, double itemMinWidth, bool scrollable, bool showPagination, VoidCallback? onCancel, String? description, VoidCallback? onClose, bool useSafeArea, | 显示宫格类型面板 | -| showGroupActionSheet | | required BuildContext context, required List items, TActionSheetAlign align, String? cancelText, bool showCancel, TActionSheetItemCallback? onSelected, bool showOverlay, bool closeOnOverlayClick, double itemHeight, double itemMinWidth, VoidCallback? onCancel, VoidCallback? onClose, bool useSafeArea, | 显示分组类型面板 | -| showListActionSheet | | required BuildContext context, required List items, TActionSheetAlign align, String? cancelText, bool showCancel, VoidCallback? onCancel, TActionSheetItemCallback? onSelected, bool showOverlay, bool closeOnOverlayClick, VoidCallback? onClose, bool useSafeArea, | 显示列表类型面板 | +| context | BuildContext | - | 上下文 | +| items | List | - | ActionSheet的项目列表 | +| align | TActionSheetAlign | TActionSheetAlign.center | 对齐方式 | +| cancelText | String? | - | 取消按钮的文本 | +| showCancel | bool | true | 是否显示取消按钮 | +| onCancel | VoidCallback? | - | 取消按钮的回调函数 | +| onSelected | TActionSheetItemCallback? | - | 选择项目时的回调函数 | +| showOverlay | bool | true | 是否显示遮罩层 | +| closeOnOverlayClick | bool | true | 点击蒙层时是否关闭 | +| onClose | VoidCallback? | - | 关闭时的回调函数 | +| useSafeArea | bool | true | 使用安全区域 | + + +### TActionSheetTheme +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| list | - | +| grid | - | +| group | - | + + +### TActionSheetAlign +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| center | - | +| left | - | +| right | - | + + +### TActionSheetItemCallback +#### 类型定义 + +```dart +typedef TActionSheetItemCallback = void Function(TActionSheetItem item, int index); +``` \ No newline at end of file diff --git a/tdesign-site/src/avatar/README.md b/tdesign-site/src/avatar/README.md index b5f0e4be4..87922dfbb 100644 --- a/tdesign-site/src/avatar/README.md +++ b/tdesign-site/src/avatar/README.md @@ -327,8 +327,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | displayText | String? | - | 纯展示类型末尾文字 | | fit | BoxFit? | - | 自定义图片对齐方式 | | icon | IconData? | - | 自定义图标 | -| key | | - | | -| onTap | Function()? | - | 操作点击事件 | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| onTap | Function()? | - | 操作点击事件 | | radius | double? | - | 自定义圆角 | | shape | TAvatarShape | TAvatarShape.circle | 头像形状 | | size | TAvatarSize | TAvatarSize.medium | 头像尺寸 | @@ -336,4 +336,38 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | type | TAvatarType | TAvatarType.normal | 头像类型 | +### TAvatarSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | - | +| medium | - | +| small | - | + + +### TAvatarType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| icon | - | +| normal | - | +| customText | - | +| display | - | +| operation | - | + + +### TAvatarShape +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| circle | - | +| square | - | + + \ No newline at end of file diff --git a/tdesign-site/src/back-top/README.md b/tdesign-site/src/back-top/README.md index a2be1fa25..15324ce21 100644 --- a/tdesign-site/src/back-top/README.md +++ b/tdesign-site/src/back-top/README.md @@ -80,11 +80,31 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | controller | ScrollController? | - | 页面滚动的控制器 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onClick | VoidCallback? | - | 按钮点击事件 | | showText | bool | false | 是否展示文字 | | style | TBackTopStyle | TBackTopStyle.circle | 样式,圆形和半圆 | | theme | TBackTopTheme | TBackTopTheme.light | 主题 | +### TBackTopTheme +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| light | - | +| dark | - | + + +### TBackTopStyle +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| circle | - | +| halfCircle | - | + + \ No newline at end of file diff --git a/tdesign-site/src/badge/README.md b/tdesign-site/src/badge/README.md index 07dbc85a6..2f629456f 100644 --- a/tdesign-site/src/badge/README.md +++ b/tdesign-site/src/badge/README.md @@ -460,19 +460,52 @@ Medium | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | +| type | TBadgeType | - | 红点样式 | | border | TBadgeBorder | TBadgeBorder.large | 红点圆角大小 | | color | Color? | - | 红点颜色 | | count | String? | - | 红点数量 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | maxCount | String? | '99' | 最大红点数量 | | message | String? | - | 消息内容 | | padding | EdgeInsetsGeometry? | - | 角标自定义padding | | showZero | bool | true | 值为0是否显示 | | size | TBadgeSize | TBadgeSize.small | 红点尺寸 | | textColor | Color? | - | 文字颜色 | -| type | TBadgeType | type | 红点样式 | | widthLarge | double | 32 | 角标大三角形宽 | | widthSmall | double | 12 | 角标小三角形宽 | +### TBadgeType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| redPoint | 红点样式 | +| message | 消息样式 | +| bubble | 气泡样式 | +| square | 方形样式 | +| subscript | 角标样式 | + + +### TBadgeBorder +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | 大圆角 8px | +| small | 小圆角 2px | + + +### TBadgeSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | 宽 20px | +| small | 宽 16px | + + \ No newline at end of file diff --git a/tdesign-site/src/button/README.md b/tdesign-site/src/button/README.md index b21519c3a..99fb16e95 100644 --- a/tdesign-site/src/button/README.md +++ b/tdesign-site/src/button/README.md @@ -755,7 +755,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | iconTextSpacing | double? | - | 自定义图标与文本之间距离 | | iconWidget | Widget? | - | 自定义图标 icon 控件 | | isBlock | bool | false | 是否为通栏按钮 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | margin | EdgeInsetsGeometry? | - | 自定义 margin | | onLongPress | TButtonEvent? | - | 长按事件 | | onTap | TButtonEvent? | - | 点击事件 | @@ -769,8 +769,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | type | TButtonType | TButtonType.fill | 类型:填充,描边,文字 | | width | double? | - | 自定义宽度 | -``` -``` ### TButtonStyle #### 默认构造方法 @@ -787,12 +785,126 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TButtonStyle.generateFillStyleByTheme | 生成不同主题的填充按钮样式 | -| TButtonStyle.generateGhostStyleByTheme | 生成不同主题的幽灵按钮样式 | -| TButtonStyle.generateOutlineStyleByTheme | 生成不同主题的描边按钮样式 | -| TButtonStyle.generateTextStyleByTheme | 生成不同主题的文本按钮样式 | +##### TButtonStyle.generateFillStyleByTheme + +生成不同主题的填充按钮样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | - | +| theme | TButtonTheme? | - | - | +| status | TButtonStatus | - | - | + + +##### TButtonStyle.generateGhostStyleByTheme + +生成不同主题的幽灵按钮样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | - | +| theme | TButtonTheme? | - | - | +| status | TButtonStatus | - | - | + + +##### TButtonStyle.generateOutlineStyleByTheme + +生成不同主题的描边按钮样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | - | +| theme | TButtonTheme? | - | - | +| status | TButtonStatus | - | - | + + +##### TButtonStyle.generateTextStyleByTheme + +生成不同主题的文本按钮样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | - | +| theme | TButtonTheme? | - | - | +| status | TButtonStatus | - | - | + + +### TButtonSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | - | +| medium | - | +| small | - | +| extraSmall | - | + + +### TButtonType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| fill | - | +| outline | - | +| text | - | +| ghost | - | + + +### TButtonShape +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| rectangle | - | +| round | - | +| square | - | +| circle | - | +| filled | - | + + +### TButtonTheme +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| defaultTheme | - | +| primary | - | +| danger | - | +| light | - | + + +### TButtonStatus +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| defaultState | - | +| active | - | +| disable | - | + + +### TButtonIconPosition +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| left | - | +| right | - | + + +### TButtonEvent +#### 类型定义 + +```dart +typedef TButtonEvent = void Function(); +``` \ No newline at end of file diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md index 0844ed4b2..7cb361fb1 100644 --- a/tdesign-site/src/calendar/README.md +++ b/tdesign-site/src/calendar/README.md @@ -465,7 +465,7 @@ Widget _buildLunar(BuildContext context) { | 名称 | 返回类型 | 参数 | 说明 | | --- | --- | --- | --- | -| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, int anchorRevision, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick, bool autoClose, bool draggable, TCalendarCellBuilder? cellBuilder, TCalendarSubtitleBuilder? subtitleBuilder, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`, 或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopupBottomDisplayPanel] + [TSlidePopupRoute] 自行组装。 | +| showPopup | | required BuildContext context, Widget? titleWidget, CalendarType type, List? initialValue, DateTime? minDate, DateTime? maxDate, DateTime? anchorDate, int anchorRevision, double? popupHeight, int firstDayOfWeek, double? cellHeight, TCalendarStyle? style, Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder, ValueListenable? popupBottomExpanded, Widget Function(VoidCallback onConfirm)? confirmBtnBuilder, void Function(List)? onConfirm, VoidCallback? onClose, void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick, bool autoClose, bool draggable, TCalendarCellBuilder? cellBuilder, TCalendarSubtitleBuilder? subtitleBuilder, TCalendarDataSource? dataSource, ValueChanged? onMonthChange, Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。 弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`, 或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。 ```dart final result = await TCalendar.showPopup( context, titleWidget: Text('请选择日期'), type: CalendarType.single, ); if (result != null) { print('选中了: $result'); } ``` 若需完全自定义布局,请直接使用 [TCalendar] + [TPopup.show] / [TPopupOptions.bottom] 自行组装。 | ``` ``` @@ -536,7 +536,4 @@ Widget _buildLunar(BuildContext context) { | --- | --- | --- | --- | | date | | - | | | isLastDayOfMonth | | - | | -| typeNotifier | | - | | - - - \ No newline at end of file +| typeNotifier | | - | | \ No newline at end of file diff --git a/tdesign-site/src/cascader/README.md b/tdesign-site/src/cascader/README.md index f7482529e..4a310a832 100644 --- a/tdesign-site/src/cascader/README.md +++ b/tdesign-site/src/cascader/README.md @@ -249,7 +249,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | initialData | String? | - | 初始化数据 | | initialIndexes | List? | - | 若为null表示全部从零开始 | | isLetterSort | bool | false | 是否开启字母排序 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onChange | MultiCascaderCallback | - | 值发生变更时触发 | | onClose | Function? | - | 选择器关闭按钮回调 | | subTitles | List? | - | 每级展示的次标题 | @@ -259,4 +259,12 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | topRadius | double? | - | 顶部圆角 | +### MultiCascaderCallback +#### 类型定义 + +```dart +typedef MultiCascaderCallback = void Function(List selected); +``` + + \ No newline at end of file diff --git a/tdesign-site/src/cell/README.md b/tdesign-site/src/cell/README.md index 163f51ad7..d345ad750 100644 --- a/tdesign-site/src/cell/README.md +++ b/tdesign-site/src/cell/README.md @@ -177,7 +177,7 @@ Widget _buildCard(BuildContext context) { | imageCircle | double? | 50 | 主图圆角,默认50(圆形) | | imageSize | double? | - | 主图尺寸 | | imageWidget | Widget? | - | 主图组件 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftIcon | IconData? | - | 左侧图标,出现在单元格标题的左侧 | | leftIconWidget | Widget? | - | 左侧图标组件 | | note | String? | - | 和标题同行的说明文字 | @@ -194,8 +194,6 @@ Widget _buildCard(BuildContext context) { | title | String? | - | 标题 | | titleWidget | Widget? | - | 标题组件 | -``` -``` ### TCellGroup #### 简介 @@ -208,15 +206,13 @@ Widget _buildCard(BuildContext context) { | builder | CellBuilder? | - | cell构建器,可自定义cell父组件,如Dismissible | | cells | List | - | 单元格列表 | | isShowLastBordered | bool? | false | 是否显示最后一个cell的下边框 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | scrollable | bool? | false | 可滚动 | | style | TCellStyle? | - | 自定义样式 | | theme | TCellGroupTheme? | TCellGroupTheme.defaultTheme | 单元格组风格。可选项:default/card | | title | String? | - | 单元格组标题 | | titleWidget | Widget? | - | 单元格组标题组件 | -``` -``` ### TCellStyle #### 简介 @@ -247,9 +243,50 @@ Widget _buildCard(BuildContext context) { #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TCellStyle.cellStyle | 生成单元格默认样式 | +##### TCellStyle.cellStyle + +生成单元格默认样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | 传递context,会生成默认样式 | + + +### TCellAlign +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| top | - | +| middle | - | +| bottom | - | + + +### TCellGroupTheme +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| defaultTheme | - | +| cardTheme | - | + + +### TCellClick +#### 类型定义 + +```dart +typedef TCellClick = void Function(TCell cell); +``` + + +### CellBuilder +#### 类型定义 + +```dart +typedef CellBuilder = Widget Function(BuildContext context, TCell cell, int index); +``` \ No newline at end of file diff --git a/tdesign-site/src/checkbox/README.md b/tdesign-site/src/checkbox/README.md index 3fde5c091..1252d2903 100644 --- a/tdesign-site/src/checkbox/README.md +++ b/tdesign-site/src/checkbox/README.md @@ -434,16 +434,16 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | backgroundColor | Color? | - | 背景颜色 | | cardMode | bool | false | 展示为卡片模式 | | checkBoxLeftSpace | double? | - | 选项框左侧间距 | -| checked | bool | false | 选中状态。默认为`false` | +| checked | bool | false | 选中状态。默认为`false` 当FuiCheckBox嵌入到FuiCheckBoxGroup的时候,这个值表示初始状态,后续的状态会由Group管理 | | contentDirection | TContentDirection | TContentDirection.right | 文字相对icon的方位 | | customContentBuilder | ContentBuilder? | - | 完全自定义内容 | | customIconBuilder | IconBuilder? | - | 自定义Checkbox显示样式 | | customSpace | EdgeInsetsGeometry? | - | 自定义组件间距 | | disableColor | Color? | - | 禁用选择颜色 | | enable | bool | true | 不可用 | -| id | String? | - | id | +| id | String? | - | id 当FuiCheckBox嵌入到FuiCheckBoxGroup内时,这个值需要赋值,否则不会被纳入Group管理 | | insetSpacing | double? | 16 | 文字和非图标侧的距离 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onCheckBoxChanged | OnCheckValueChanged? | - | 切换监听 | | selectColor | Color? | - | 选择颜色 | | showDivider | bool | true | 是否展示分割线 | @@ -459,8 +459,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | titleFont | Font? | - | 标题字体大小 | | titleMaxLine | int? | - | 标题的行数 | -``` -``` ### TCheckboxGroup #### 默认构造方法 @@ -468,12 +466,12 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | checkedIds | List? | - | 勾选的CheckBox id列表 | -| child | | - | | +| child | Widget | - | 可以是任意包含TCheckBox的容器,比如: ``` Row( children: [ TCheckBox(), TCheckBox(), ... ] ) ``` | | contentDirection | TContentDirection? | - | 文字相对icon的方位 | | controller | TCheckboxGroupController? | - | 可以通过控制器操作勾选状态 | | customContentBuilder | ContentBuilder? | - | CheckBox完全自定义内容 | | customIconBuilder | IconBuilder? | - | 自定义选择icon的样式 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | maxChecked | int? | - | 最多可以勾选多少 | | onChangeGroup | OnGroupChange? | - | 状态变化监听器 | | onOverloadChecked | VoidCallback? | - | 超过最大可勾选的个数 | @@ -482,4 +480,85 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | titleMaxLine | int? | - | CheckBox标题的行数 | +### TCheckboxStyle +#### 简介 +选择框的样式 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| circle | - | +| square | - | +| check | - | + + +### TContentDirection +#### 简介 +内容相对icon的位置,上、下、左、右,默认内容在icon的右边 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| left | - | +| right | - | + + +### TCheckBoxSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | - | +| small | - | + + +### IconBuilder +#### 简介 +自定义Icon +#### 类型定义 + +```dart +typedef IconBuilder = Widget? Function(BuildContext context, bool checked); +``` + + +### ContentBuilder +#### 简介 +自定义Content +#### 类型定义 + +```dart +typedef ContentBuilder = Widget Function(BuildContext context, bool checked, String? content); +``` + + +### OnCheckValueChanged +#### 类型定义 + +```dart +typedef OnCheckValueChanged = void Function(bool selected); +``` + + +### OnGroupChange +#### 简介 +CheckBoxGroup变化监听器 +#### 类型定义 + +```dart +typedef OnGroupChange = void Function(List checkedIds); +``` + + +### OnCheckBoxGroupChange +#### 类型定义 + +```dart +typedef OnCheckBoxGroupChange = void Function(List ids); +``` + + \ No newline at end of file diff --git a/tdesign-site/src/collapse/README.md b/tdesign-site/src/collapse/README.md index a1f767ff2..0174ec848 100644 --- a/tdesign-site/src/collapse/README.md +++ b/tdesign-site/src/collapse/README.md @@ -177,16 +177,42 @@ Card Style 卡片样式 | animationDuration | Duration | kThemeAnimationDuration | 折叠面板列表的动画时长 | | children | List | - | 折叠面板列表的子组件 | | elevation | double | 0 | 折叠面板列表的阴影 | -| expansionCallback | ExpansionPanelCallback? | - | 折叠面板列表的回调函数; | -| key | | - | | -| style | TCollapseStyle | TCollapseStyle.block | 折叠面板列表的样式 | +| expansionCallback | ExpansionPanelCallback? | - | 折叠面板列表的回调函数; 回调时,入参为当前点击的折叠面板的索引 index 和是否展开的状态 isExpanded | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| style | TCollapseStyle | TCollapseStyle.block | 折叠面板列表的样式 - [TCollapseStyle.block] 通栏风格 - [TCollapseStyle.card] 卡片风格 | + +#### 公开属性 + +| 属性 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| initialOpenPanelValue | Object? | - | 折叠面板列表的默认展开面板的值; 当使用 [TCollapse.accordion] 时,此值生效 | #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TCollapse.accordion | | +##### TCollapse.accordion + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| children | List | - | 折叠面板列表的子组件 | +| style | TCollapseStyle | TCollapseStyle.block | 折叠面板列表的样式 - [TCollapseStyle.block] 通栏风格 - [TCollapseStyle.card] 卡片风格 | +| expansionCallback | ExpansionPanelCallback? | - | 折叠面板列表的回调函数; 回调时,入参为当前点击的折叠面板的索引 index 和是否展开的状态 isExpanded | +| animationDuration | Duration | kThemeAnimationDuration | 折叠面板列表的动画时长 | +| elevation | double | 0 | 折叠面板列表的阴影 | +| initialOpenPanelValue | Object? | - | 折叠面板列表的默认展开面板的值; 当使用 [TCollapse.accordion] 时,此值生效 | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | + + +### TCollapseStyle +#### 简介 +折叠面板的组件样式 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| block | Block 通栏风格 | +| card | Card 卡片风格 | \ No newline at end of file diff --git a/tdesign-site/src/dialog/README.md b/tdesign-site/src/dialog/README.md index 287b1a962..68ee496dc 100644 --- a/tdesign-site/src/dialog/README.md +++ b/tdesign-site/src/dialog/README.md @@ -709,19 +709,19 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | backgroundColor | Color? | - | 背景颜色 | -| buttonStyle | | TDialogButtonStyle.normal | | +| buttonStyle | TDialogButtonStyle | TDialogButtonStyle.normal | - | | buttonWidget | Widget? | - | 自定义按钮 | | content | String? | - | 内容 | | contentColor | Color? | - | 内容颜色 | | contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 | | contentWidget | Widget? | - | 内容Widget | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftBtn | TDialogButtonOptions? | - | 左侧按钮配置 | -| leftBtnAction | Function()? | - | 左侧按钮默认点击 | +| leftBtnAction | Function()? | - | 左侧按钮默认点击 | | padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 | | radius | double | 12.0 | 圆角 | | rightBtn | TDialogButtonOptions? | - | 右侧按钮配置 | -| rightBtnAction | Function()? | - | 右侧按钮默认点击 | +| rightBtnAction | Function()? | - | 右侧按钮默认点击 | | showCloseButton | bool? | - | 显示右上角关闭按钮 | | title | String? | - | 标题 | | titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | @@ -730,21 +730,36 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TAlertDialog.vertical | 纵向按钮排列的对话框 +##### TAlertDialog.vertical - [buttons]参数是必须的,纵向按钮默认样式都是[TButtonTheme.primary] | +纵向按钮排列的对话框 + + [buttons]参数是必须的,纵向按钮默认样式都是[TButtonTheme.primary] + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| buttons | List | - | - | +| backgroundColor | Color? | - | 背景颜色 | +| radius | double | 12.0 | 圆角 | +| title | String? | - | 标题 | +| titleColor | Color? | - | 标题颜色 | +| titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | +| contentWidget | Widget? | - | 内容Widget | +| content | String? | - | 内容 | +| contentColor | Color? | - | 内容颜色 | +| contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 | +| showCloseButton | bool? | - | 显示右上角关闭按钮 | +| padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 | +| buttonWidget | Widget? | - | 自定义按钮 | -``` -``` ### TConfirmDialog #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| action | Function()? | - | 点击 | +| action | Function()? | - | 点击 | | backgroundColor | Color? | - | 背景颜色 | | buttonStyle | TDialogButtonStyle | TDialogButtonStyle.normal | 按钮样式 | | buttonStyleCustom | TButtonStyle? | - | 按钮自定义样式属性,背景色、边框... | @@ -755,35 +770,31 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | contentColor | Color? | - | 内容颜色 | | contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 | | contentWidget | Widget? | - | 内容Widget | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 | | radius | double | 12.0 | 圆角 | | showCloseButton | bool? | - | 右上角关闭按钮 | | title | String? | - | 标题 | | titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | | titleColor | Color? | - | 标题颜色 | -| width | | - | | +| width | double? | - | - | -``` -``` ### TDialogButtonOptions #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| action | Function()? | - | 点击操作 | +| action | Function()? | - | 点击操作 | | fontWeight | FontWeight? | - | 字体粗细 | -| height | double? | - | 按钮高度 | -| style | TButtonStyle? | - | 按钮样式 | +| height | double? | - | 按钮高度 建议使用默认高度 | +| style | TButtonStyle? | - | 按钮样式 设置单个按钮的样式会覆盖Dialog的默认样式 | | theme | TButtonTheme? | - | 按钮类型 | | title | String | - | 标题内容 | | titleColor | Color? | - | 标题颜色 | | titleSize | double? | - | 字体大小 | | type | TButtonType? | - | 按钮类型 | -``` -``` ### TDialogScaffold #### 默认构造方法 @@ -792,25 +803,21 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | --- | --- | --- | --- | | backgroundColor | Color? | - | 背景色 | | body | Widget | - | Dialog主体 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | radius | double | 12.0 | 圆角 | | showCloseButton | bool? | - | 显示右上角关闭按钮 | | width | double? | - | 弹窗宽度 | -``` -``` ### TDialogTitle #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | title | String? | - | 标题文字 | | titleColor | Color? | - | 标题颜色 | -``` -``` ### TDialogContent #### 默认构造方法 @@ -819,10 +826,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | --- | --- | --- | --- | | content | String? | - | 标题文字 | | contentColor | Color? | - | 标题颜色 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | -``` -``` ### TDialogInfoWidget #### 默认构造方法 @@ -833,38 +838,32 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | contentColor | Color? | - | 内容颜色 | | contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 | | contentWidget | Widget? | - | 内容Widget | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | padding | EdgeInsetsGeometry? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容的内边距 | | title | String? | - | 标题 | | titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | | titleColor | Color? | - | 标题颜色 | -``` -``` ### HorizontalNormalButtons #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftBtn | TDialogButtonOptions | - | 左按钮 | | rightBtn | TDialogButtonOptions | - | 右按钮 | -``` -``` ### HorizontalTextButtons #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftBtn | TDialogButtonOptions | - | 左按钮 | | rightBtn | TDialogButtonOptions | - | 右按钮 | -``` -``` ### TDialogButton #### 默认构造方法 @@ -880,12 +879,10 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | buttonType | TButtonType? | - | 按钮类型 | | height | double? | 40.0 | 按钮高度 | | isBlock | bool | true | 按钮高度 | -| key | | - | | -| onPressed | Function() | - | 点击 | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| onPressed | Function() | - | 点击 | | width | double? | - | 按钮宽度 | -``` -``` ### TImageDialog #### 默认构造方法 @@ -899,7 +896,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | contentWidget | Widget? | - | 内容Widget | | image | Image | - | 图片 | | imagePosition | TDialogImagePosition? | TDialogImagePosition.top | 图片位置 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftBtn | TDialogButtonOptions? | - | 左侧按钮配置 | | padding | EdgeInsets? | - | 内容内边距 | | radius | double | 12.0 | 圆角 | @@ -909,8 +906,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | titleAlignment | AlignmentGeometry? | - | 标题对齐模式 | | titleColor | Color? | - | 标题颜色 | -``` -``` ### TInputDialog #### 默认构造方法 @@ -924,7 +919,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | contentWidget | Widget? | - | 内容Widget | | customInputWidget | Widget? | - | 自定义输入框 | | hintText | String? | '' | 输入提示 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftBtn | TDialogButtonOptions? | - | 左侧按钮配置 | | padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 | | radius | double | 12.0 | 圆角 | @@ -936,4 +931,29 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | titleColor | Color? | - | 标题颜色 | +### TDialogButtonStyle +#### 简介 +Dialog按钮样式 + + 用于在Dialog层面配置按钮样式 + Dialog内支持配置每个按钮的样式 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| normal | - | +| text | - | + + +### TDialogImagePosition +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| top | - | +| middle | - | + + \ No newline at end of file diff --git a/tdesign-site/src/divider/README.md b/tdesign-site/src/divider/README.md index 56fabf0ee..37251b43a 100644 --- a/tdesign-site/src/divider/README.md +++ b/tdesign-site/src/divider/README.md @@ -152,7 +152,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | height | double | 0.5 | 高度,横向线条使用 | | hideLine | bool | false | 隐藏线条,使用纯文本分割 | | isDashed | bool | false | 是否为虚线 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | margin | EdgeInsetsGeometry? | - | 外部填充 | | text | String? | - | 文本字符串,使用默认样式 | | textStyle | TextStyle? | - | 自定义文本样式 | @@ -160,4 +160,15 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | width | double? | - | 宽度,需要竖向线条时使用 | +### TextAlignment +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| left | - | +| center | - | +| right | - | + + \ No newline at end of file diff --git a/tdesign-site/src/drawer/README.md b/tdesign-site/src/drawer/README.md index 1e4ad50dd..1bdf26a3c 100644 --- a/tdesign-site/src/drawer/README.md +++ b/tdesign-site/src/drawer/README.md @@ -292,11 +292,11 @@ Widget _buildBottomSimple(BuildContext context) { | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | +| context | BuildContext | - | 上下文 | | backgroundColor | Color? | - | 组件背景颜色 | | bordered | bool? | true | 是否显示边框 | | closeOnOverlayClick | bool? | true | 点击蒙层时是否关闭抽屉 | | contentWidget | Widget? | - | 自定义内容,优先级高于[items]/[footer]/[title] | -| context | BuildContext | context | 上下文 | | drawerTop | double? | - | 距离顶部的距离 | | footer | Widget? | - | 抽屉的底部 | | hover | bool? | true | 是否开启点击反馈 | @@ -312,8 +312,6 @@ Widget _buildBottomSimple(BuildContext context) { | visible | bool? | - | 组件是否可见 | | width | double? | 280 | 宽度 | -``` -``` ### TDrawerWidget #### 简介 @@ -330,15 +328,13 @@ Widget _buildBottomSimple(BuildContext context) { | hover | bool? | true | 是否开启点击反馈 | | isShowLastBordered | bool? | true | 是否显示最后一行分割线 | | items | List? | - | 抽屉里的列表项 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onItemClick | TDrawerItemClickCallback? | - | 点击抽屉里的列表项触发 | | style | TCellStyle? | - | 列表自定义样式 | | title | String? | - | 抽屉的标题 | | titleWidget | Widget? | - | 抽屉的标题组件 | | width | double? | 280 | 宽度 | -``` -``` ### TDrawerItem #### 简介 @@ -352,4 +348,24 @@ Widget _buildBottomSimple(BuildContext context) { | title | String? | - | 每列标题 | +### TDrawerPlacement +#### 简介 +抽屉方向 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| left | - | +| right | - | + + +### TDrawerItemClickCallback +#### 类型定义 + +```dart +typedef TDrawerItemClickCallback = void Function(int index, TDrawerItem item); +``` + + \ No newline at end of file diff --git a/tdesign-site/src/dropdown-menu/README.md b/tdesign-site/src/dropdown-menu/README.md index ec977c8ed..cfa91abfd 100644 --- a/tdesign-site/src/dropdown-menu/README.md +++ b/tdesign-site/src/dropdown-menu/README.md @@ -283,7 +283,7 @@ TDropdownMenu _buildGroup(BuildContext context) { | height | double? | 48 | menu的高度 | | isScrollable | bool? | false | 是否开启滚动列表 | | items | List? | - | 下拉菜单 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | labelBuilder | LabelBuilder? | - | 自定义标签内容 | | onMenuClosed | ValueChanged? | - | 关闭菜单事件 | | onMenuOpened | ValueChanged? | - | 展开菜单事件 | @@ -291,8 +291,6 @@ TDropdownMenu _buildGroup(BuildContext context) { | tabBarAlign | MainAxisAlignment? | MainAxisAlignment.center | [TDropdownItem.label]和[arrowIcon]/[TDropdownItem.arrowIcon]的对齐方式 | | width | double? | - | menu的宽度 | -``` -``` ### TDropdownItem #### 简介 @@ -306,7 +304,7 @@ TDropdownMenu _buildGroup(BuildContext context) { | builder | TDropdownItemContentBuilder? | - | 完全自定义展示内容 | | controller | TDropdownItemController? | - | 下拉菜单控制器 | | disabled | bool? | false | 是否禁用 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | label | String? | - | 标题 | | maxHeight | double? | - | 内容最大高度 | | minHeight | double? | - | 内容最小高度 | @@ -320,8 +318,12 @@ TDropdownMenu _buildGroup(BuildContext context) { | tabBarFlex | int? | 1 | 该item在menu上的宽度占比,仅在[TDropdownMenu.isScrollable]为false时有效 | | tabBarWidth | double? | - | 该item在menu上的宽度,仅在[TDropdownMenu.isScrollable]为true时有效 | -``` -``` +#### 静态成员 + +| 名称 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| operateHeight | double | - | - | + ### TDropdownItemOption #### 简介 @@ -338,11 +340,58 @@ TDropdownMenu _buildGroup(BuildContext context) { | selectedColor | Color? | - | 选中颜色 | | value | String | - | 选项值 | -``` -``` ### TDropdownItemController #### 简介 下拉菜单控制器 +### TDropdownMenuDirection +#### 简介 +菜单展开方向 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| down | 向下 | +| up | 向上 | +| auto | 根据内容高度动态展示方向 | + + +### TDropdownItemContentBuilder +#### 类型定义 + +```dart +typedef TDropdownItemContentBuilder = Widget Function(BuildContext context, _TDropdownItemState itemState, TDropdownPopup? popupState); +``` + + +### TDropdownItemOptionsCallback +#### 类型定义 + +```dart +typedef TDropdownItemOptionsCallback = void Function(List? options); +``` + + +### TDropdownItemBuilder +#### 简介 +下拉菜单构建器 +#### 类型定义 + +```dart +typedef TDropdownItemBuilder = List Function(BuildContext context); +``` + + +### LabelBuilder +#### 简介 +自定义标签内容 +#### 类型定义 + +```dart +typedef LabelBuilder = Widget Function(BuildContext context, String label, bool isOpened, int index); +``` + + \ No newline at end of file diff --git a/tdesign-site/src/empty/README.md b/tdesign-site/src/empty/README.md index 8f5075b70..b7ed502d3 100644 --- a/tdesign-site/src/empty/README.md +++ b/tdesign-site/src/empty/README.md @@ -131,11 +131,29 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | emptyTextFont | Font? | - | 描述文字大小 | | icon | IconData? | TIcons.info_circle_filled | 图标 | | image | Widget? | - | 展示图片 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onTapEvent | TTapEvent? | - | 点击事件 | | operationText | String? | - | 操作按钮文案 | | operationTheme | TButtonTheme? | - | 操作按钮文案主题色 | | type | TEmptyType | TEmptyType.plain | 类型,为operation有操作按钮,plain无按钮 | +### TEmptyType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| plain | - | +| operation | - | + + +### TTapEvent +#### 类型定义 + +```dart +typedef TTapEvent = void Function(); +``` + + \ No newline at end of file diff --git a/tdesign-site/src/fab/README.md b/tdesign-site/src/fab/README.md index c24020665..4badca22f 100644 --- a/tdesign-site/src/fab/README.md +++ b/tdesign-site/src/fab/README.md @@ -167,7 +167,7 @@ Fab Size 悬浮按钮尺寸 | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | icon | Icon? | - | 图标 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onClick | VoidCallback? | - | 点击事件 | | shape | TFabShape | TFabShape.circle | 形状 | | size | TFabSize | TFabSize.large | 大小 | @@ -175,4 +175,38 @@ Fab Size 悬浮按钮尺寸 | theme | TFabTheme | TFabTheme.defaultTheme | 主题 | +### TFabTheme +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| primary | - | +| defaultTheme | - | +| light | - | +| danger | - | + + +### TFabShape +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| circle | - | +| square | - | + + +### TFabSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| large | - | +| medium | - | +| small | - | +| extraSmall | - | + + \ No newline at end of file diff --git a/tdesign-site/src/footer/README.md b/tdesign-site/src/footer/README.md index d6323dbda..d761bcc58 100644 --- a/tdesign-site/src/footer/README.md +++ b/tdesign-site/src/footer/README.md @@ -117,13 +117,24 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | +| type | TFooterType | - | 样式 | | height | double? | - | 自定义图片高 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | links | List | const [] | 链接 | | logo | String? | - | 品牌图片 | | text | String | '' | 文字 | -| type | TFooterType | type | 样式 | | width | double? | - | 自定义图片宽 | +### TFooterType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| text | 文字样式 | +| link | 链接样式 | +| brand | 品牌样式 | + + \ No newline at end of file diff --git a/tdesign-site/src/form/README.md b/tdesign-site/src/form/README.md index 25d02941b..02c02781a 100644 --- a/tdesign-site/src/form/README.md +++ b/tdesign-site/src/form/README.md @@ -719,24 +719,22 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | data | Map | - | 表单数据 | | disabled | bool | false | 是否禁用整个表单 | | errorMessage | Object? | - | 表单信息错误信息配置 | -| formContentAlign | TextAlign | TextAlign.left | 表单内容对齐方式: 左对齐、右对齐、居中对齐 | +| formContentAlign | TextAlign | TextAlign.left | 表单内容对齐方式: 左对齐、右对齐、居中对齐 可选项: left/right/center 默认为左对齐 优先级低于 TFormItem 的对齐 API TODO: TStepper TRate 等组件没用实现通用性 | | formController | FormController? | - | 表单控制器 | -| formLabelAlign | TextAlign? | TextAlign.left | 表单字段标签的对齐方式: | -| formShowErrorMessage | bool? | true | 校验不通过时,是否显示错误提示信息,统一控制全部表单项 | +| formLabelAlign | TextAlign? | TextAlign.left | 表单字段标签的对齐方式: 左对齐、右对齐、顶部对齐 可选项: left/right/top TODO: 表单总体标签对齐方式 | +| formShowErrorMessage | bool? | true | 校验不通过时,是否显示错误提示信息,统一控制全部表单项 如果希望控制单个表单项,请给 FormItem 设置该属性 | | isHorizontal | bool | true | 表单排列方式是否为 水平方向 | | items | List | - | 表单内容 items | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | labelWidth | double? | 20.0 | 可以整体设置 label 标签宽度 | | onReset | Function? | - | 表单重置时触发 | | onSubmit | Function | - | 表单提交时触发 | -| preventSubmitDefault | bool? | true | 是否阻止表单提交默认事件(表单提交默认事件会刷新页面) | +| preventSubmitDefault | bool? | true | 是否阻止表单提交默认事件(表单提交默认事件会刷新页面) 设置为 true 可以避免刷新 | | requiredMark | bool? | true | 是否显示必填符号(*),默认显示 | | rules | Map | - | 整个表单字段校验规则 | -| scrollToFirstError | String? | - | 表单校验不通过时,是否自动滚动到第一个校验不通过的字段,平滑滚动或是瞬间直达。 | +| scrollToFirstError | String? | - | 表单校验不通过时,是否自动滚动到第一个校验不通过的字段,平滑滚动或是瞬间直达。 值为空则表示不滚动。可选项:''/smooth/auto | | submitWithWarningMessage | bool? | false | 【讨论中】当校验结果只有告警信息时,是否触发 submit 提交事件 | -``` -``` ### TFormItem #### 默认构造方法 @@ -746,20 +744,20 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | additionInfo | String? | - | TInput的辅助信息 | | backgroundColor | Color? | - | 背景色 | | child | Widget? | - | 表单子组件 | -| contentAlign | TextAlign? | - | 表单显示内容对齐方式: | -| formItemNotifier | | - | | +| contentAlign | TextAlign? | - | 表单显示内容对齐方式: left、right、top TODO: TStepper TRate 等组件没用实现通用性 | +| formItemNotifier | FormItemNotifier? | - | - | | formRules | List? | - | 整个表单的校验规则 | | help | String? | - | TInput 默认显示文字 | -| hintText | null | '' | 提示内容 | +| hintText | - | '' | 提示内容 | | indicator | bool? | - | TTextarea 的属性,指示器 | | itemRule | List? | - | 表单项验证规则 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | label | String? | - | 表单项标签左侧展示的内容 | -| labelAlign | TextAlign? | - | TODO: item 标签对齐方式 | +| labelAlign | TextAlign? | - | TODO: item 标签对齐方式 可选: left、right、top | | labelWidget | Widget? | - | 自定义标签 | | labelWidth | double? | - | 标签宽度,如果提供则覆盖Form的labelWidth | | name | String? | - | 表单字段名称 | -| radios | | - | | +| radios | Map? | - | - | | requiredMark | bool? | true | 是否显示必填标记(*) | | select | String | '' | 选择器 适用于日期选择器等 | | selectFn | Function? | - | 选择器方法 适用于日期选择器等 | @@ -767,8 +765,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | tipAlign | TextAlign? | - | 组件提示内容对齐方式 | | type | TFormItemType | - | 表格单元需要使用的组件类型 | -``` -``` ### TFormValidation #### 默认构造方法 @@ -780,4 +776,22 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | validate | String? Function(dynamic) | - | 校验方法 | +### TFormItemType +#### 简介 +表格单元选用组件类型的枚举 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| input | - | +| radios | - | +| dateTimePicker | - | +| cascader | - | +| stepper | - | +| rate | - | +| textarea | - | +| upLoadImg | - | + + \ No newline at end of file diff --git a/tdesign-site/src/image-viewer/README.md b/tdesign-site/src/image-viewer/README.md index 4f159dc3e..f48679c44 100644 --- a/tdesign-site/src/image-viewer/README.md +++ b/tdesign-site/src/image-viewer/README.md @@ -73,12 +73,42 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; #### 静态方法 -| 名称 | 返回类型 | 参数 | 说明 | +##### TImageViewer.showImageViewer + +显示图片预览 + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| showImageViewer | | required BuildContext context, required List images, List? labels, bool? closeBtn, bool? deleteBtn, bool? showIndex, bool? loop, bool? autoplay, int? duration, Color? bgColor, Color? navBarBgColor, Color? iconColor, TextStyle? labelStyle, TextStyle? indexStyle, Color? modalBarrierColor, bool? barrierDismissible, int? defaultIndex, double? width, double? height, OnIndexChange? onIndexChange, OnClose? onClose, OnDelete? onDelete, bool? ignoreDeleteError, OnImageTap? onTap, OnLongPress? onLongPress, LeftItemBuilder? leftItemBuilder, RightItemBuilder? rightItemBuilder, | 显示图片预览 | +| context | BuildContext | - | - | +| images | List | - | 图片数组 | +| labels | List? | - | 图片描述 | +| closeBtn | bool? | true | 是否展示关闭按钮 | +| deleteBtn | bool? | false | 是否显示删除操作 | +| showIndex | bool? | false | 是否显示页码 | +| loop | bool? | false | 图片是否循环 | +| autoplay | bool? | false | 图片轮播是否自动播放 | +| duration | int? | - | 自动播放间隔 | +| bgColor | Color? | - | 背景色 | +| navBarBgColor | Color? | - | 导航栏背景色 | +| iconColor | Color? | - | 图标颜色 | +| labelStyle | TextStyle? | - | label文字样式 | +| indexStyle | TextStyle? | - | 页码样式 | +| modalBarrierColor | Color? | - | - | +| barrierDismissible | bool? | - | - | +| defaultIndex | int? | - | 默认预览图片所在的下标 | +| width | double? | - | 图片宽度 | +| height | double? | - | 图片高度 | +| onIndexChange | OnIndexChange? | - | 预览图片切换回调 | +| onClose | OnClose? | - | 关闭点击 | +| onDelete | OnDelete? | - | 删除点击 | +| ignoreDeleteError | bool? | - | 是否忽略单张图片删除错误提示 | +| onTap | OnImageTap? | - | 点击图片 | +| onLongPress | OnLongPress? | - | 长按图片 | +| leftItemBuilder | LeftItemBuilder? | - | 左侧自定义操作 | +| rightItemBuilder | RightItemBuilder? | - | 右侧自定义操作 | -``` -``` ### TImageViewerWidget #### 默认构造方法 @@ -96,7 +126,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | ignoreDeleteError | bool? | false | 是否忽略单张图片删除错误提示 | | images | List | - | 图片数组 | | indexStyle | TextStyle? | - | 页码样式 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | labels | List? | - | 图片描述 | | labelStyle | TextStyle? | - | label文字样式 | | leftItemBuilder | LeftItemBuilder? | - | 左侧自定义操作 | @@ -112,4 +142,60 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | width | double? | - | 图片宽度 | +### OnIndexChange +#### 类型定义 + +```dart +typedef OnIndexChange = Function(int index); +``` + + +### OnClose +#### 类型定义 + +```dart +typedef OnClose = Function(int index); +``` + + +### OnDelete +#### 类型定义 + +```dart +typedef OnDelete = Function(int index); +``` + + +### OnImageTap +#### 类型定义 + +```dart +typedef OnImageTap = Function(int index); +``` + + +### OnLongPress +#### 类型定义 + +```dart +typedef OnLongPress = Function(int index); +``` + + +### LeftItemBuilder +#### 类型定义 + +```dart +typedef LeftItemBuilder = Widget Function(BuildContext context, int index); +``` + + +### RightItemBuilder +#### 类型定义 + +```dart +typedef RightItemBuilder = Widget Function(BuildContext context, int index); +``` + + \ No newline at end of file diff --git a/tdesign-site/src/image/README.md b/tdesign-site/src/image/README.md index 03464495a..ee95db5cb 100644 --- a/tdesign-site/src/image/README.md +++ b/tdesign-site/src/image/README.md @@ -1906,33 +1906,48 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| alignment | | Alignment.center | | +| alignment | AlignmentGeometry | Alignment.center | - | | assetUrl | String? | - | 本地素材地址 | -| cacheHeight | | - | | -| cacheWidth | | - | | -| centerSlice | | - | | -| color | | - | | -| colorBlendMode | | - | | -| errorBuilder | | - | | +| cacheHeight | int? | - | - | +| cacheWidth | int? | - | - | +| centerSlice | Rect? | - | - | +| color | Color? | - | - | +| colorBlendMode | BlendMode? | - | - | +| errorBuilder | ImageErrorWidgetBuilder? | - | - | | errorWidget | Widget? | - | 失败自定义提示 | -| excludeFromSemantics | | false | | -| filterQuality | | FilterQuality.low | | +| excludeFromSemantics | bool | false | - | +| filterQuality | FilterQuality | FilterQuality.low | - | | fit | BoxFit? | - | 适配样式 | | frameBuilder | ImageFrameBuilder? | - | 以下系统Image属性,释义请参考系统[Image]中注释 | -| gaplessPlayback | | false | | +| gaplessPlayback | bool | false | - | | height | double? | - | 自定义高 | | imageFile | File? | - | 图片文件路径 | | imgUrl | String? | - | 图片地址 | -| isAntiAlias | | false | | -| key | | - | | -| loadingBuilder | | - | | +| isAntiAlias | bool | false | - | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | +| loadingBuilder | ImageLoadingBuilder? | - | - | | loadingWidget | Widget? | - | 加载自定义提示 | -| matchTextDirection | | false | | -| opacity | | - | | -| repeat | | ImageRepeat.noRepeat | | -| semanticLabel | | - | | +| matchTextDirection | bool | false | - | +| opacity | Animation? | - | - | +| repeat | ImageRepeat | ImageRepeat.noRepeat | - | +| semanticLabel | String? | - | - | | type | TImageType | TImageType.roundedSquare | 图片类型 | | width | double? | - | 自定义宽 | +### TImageType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| clip | 裁剪 | +| fitHeight | 适应高 | +| fitWidth | 适应宽 | +| stretch | 拉伸 | +| square | 方形, | +| roundedSquare | 圆角方形 | +| circle | 圆形 | + + \ No newline at end of file diff --git a/tdesign-site/src/indexes/README.md b/tdesign-site/src/indexes/README.md index 991028b4b..5deedd0b4 100644 --- a/tdesign-site/src/indexes/README.md +++ b/tdesign-site/src/indexes/README.md @@ -36,28 +36,22 @@ Widget _buildSimple(BuildContext context) { theme: TButtonTheme.primary, type: TButtonType.outline, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.right, - modalTop: renderBox?.size.height, - builder: (context) { - return TIndexes( + TPopup.show( + context, + options: TPopupOptions.right( + width: 280, + inset: TPopupRightInset(top: renderBox?.size.height ?? 0), + child: TIndexes( indexList: indexList, builderContent: (context, index) { final list = _list.firstWhere( (element) => element['index'] == index)['children'] as List; return TCellGroup( - cells: list - .map((e) => TCell( - title: e, - )) - .toList(), + cells: list.map((e) => TCell(title: e)).toList(), ); }, - ); - }, - ), + )), ); }, ); @@ -80,28 +74,22 @@ Widget _buildSimple(BuildContext context) { theme: TButtonTheme.primary, type: TButtonType.outline, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.right, - modalTop: renderBox?.size.height, - builder: (context) { - return TIndexes( + TPopup.show( + context, + options: TPopupOptions.right( + width: 280, + inset: TPopupRightInset(top: renderBox?.size.height ?? 0), + child: TIndexes( indexList: indexList, builderContent: (context, index) { final list = _list.firstWhere( (element) => element['index'] == index)['children'] as List; return TCellGroup( - cells: list - .map((e) => TCell( - title: e, - )) - .toList(), + cells: list.map((e) => TCell(title: e)).toList(), ); }, - ); - }, - ), + )), ); }, ); @@ -127,12 +115,12 @@ Widget _buildOther(BuildContext context) { theme: TButtonTheme.primary, type: TButtonType.outline, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.right, - modalTop: renderBox?.size.height, - builder: (context) { - return TIndexes( + TPopup.show( + context, + options: TPopupOptions.right( + width: 280, + inset: TPopupRightInset(top: renderBox?.size.height ?? 0), + child: TIndexes( indexList: indexList, capsuleTheme: true, builderContent: (context, index) { @@ -140,16 +128,10 @@ Widget _buildOther(BuildContext context) { (element) => element['index'] == index)['children'] as List; return TCellGroup( - cells: list - .map((e) => TCell( - title: e, - )) - .toList(), + cells: list.map((e) => TCell(title: e)).toList(), ); }, - ); - }, - ), + )), ); }, ); @@ -172,12 +154,12 @@ Widget _buildOther(BuildContext context) { theme: TButtonTheme.primary, type: TButtonType.outline, onTap: () { - Navigator.of(context).push( - TSlidePopupRoute( - slideTransitionFrom: SlideTransitionFrom.right, - modalTop: renderBox?.size.height, - builder: (context) { - return TIndexes( + TPopup.show( + context, + options: TPopupOptions.right( + width: 280, + inset: TPopupRightInset(top: renderBox?.size.height ?? 0), + child: TIndexes( indexList: indexList, capsuleTheme: true, builderContent: (context, index) { @@ -185,16 +167,10 @@ Widget _buildOther(BuildContext context) { (element) => element['index'] == index)['children'] as List; return TCellGroup( - cells: list - .map((e) => TCell( - title: e, - )) - .toList(), + cells: list.map((e) => TCell(title: e)).toList(), ); }, - ); - }, - ), + )), ); }, ); @@ -218,7 +194,7 @@ Widget _buildOther(BuildContext context) { | capsuleTheme | bool? | false | 锚点是否为胶囊式样式 | | indexList | List? | - | 索引字符列表。不传默认 A-Z | | indexListMaxHeight | double? | 0.8 | 索引列表最大高度(父容器高度的百分比,默认 0.8) | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onChange | void Function(String index)? | - | 索引发生变更时触发事件 | | onSelect | void Function(String index)? | - | 点击侧边栏时触发事件 | | reverse | bool? | false | 反方向滚动置顶 | @@ -226,8 +202,6 @@ Widget _buildOther(BuildContext context) { | sticky | bool? | true | 锚点是否吸顶 | | stickyOffset | double? | 0 | 锚点吸顶时与顶部的距离 | -``` -``` ### TIndexesAnchor #### 简介 @@ -239,12 +213,10 @@ Widget _buildOther(BuildContext context) { | activeIndex | ValueNotifier | - | 选中索引 | | builderAnchor | Widget? Function(BuildContext context, String index, bool isPinnedToTop)? | - | 索引锚点构建 | | capsuleTheme | bool | - | 是否为胶囊式样式 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | sticky | bool | - | 索引是否吸顶 | | text | String | - | 锚点文本 | -``` -``` ### TIndexesList #### 简介 @@ -257,7 +229,7 @@ Widget _buildOther(BuildContext context) { | builderIndex | Widget Function(BuildContext context, String index, bool isActive)? | - | 索引文本自定义构建,包括索引激活左侧提示 | | indexList | List | - | 索引字符列表。不传默认 A-Z | | indexListMaxHeight | double | 0.8 | 索引列表最大高度(父容器高度的百分比,默认0.8) | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | onSelect | void Function(String newIndex, String oldIndex) | - | 点击侧边栏时触发事件 | diff --git a/tdesign-site/src/input/README.md b/tdesign-site/src/input/README.md index 32958cd3d..c66dd4440 100644 --- a/tdesign-site/src/input/README.md +++ b/tdesign-site/src/input/README.md @@ -1042,7 +1042,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | inputDecoration | InputDecoration? | - | 自定义输入框样式,默认圆角 | | inputFormatters | List? | - | 显示输入内容,如限制长度(LengthLimitingTextInputFormatter(6)) | | inputType | TextInputType? | - | 键盘类型,数字、字母 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | labelWidget | Widget? | - | leftLabel右侧组件,支持自定义 | | leftContentSpace | double? | - | 输入框内容左侧间距 | | leftIcon | Widget? | - | 带图标的输入框 | @@ -1067,7 +1067,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | selectionControls | TextSelectionControls? | - | 自定义选择控制器 | | showBottomDivider | bool | true | 是否展示底部分割线 | | size | TInputSize | TInputSize.large | 输入框规格 | -| spacer | TInputSpacer | - | 组件各模块间间距 | +| spacer | TInputSpacer? | - | 组件各模块间间距 | | textAlign | TextAlign? | - | 文字对齐方向 | | textInputBackgroundColor | Color? | - | 文本框背景色 | | textStyle | TextStyle? | - | 文本颜色 | @@ -1075,4 +1075,39 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | width | double? | - | 输入框宽度(TCardStyle时必须设置该参数) | +### TInputType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| normal | - | +| twoLine | - | +| longText | - | +| special | - | +| normalMaxTwoLine | - | +| cardStyle | - | + + +### TInputSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| small | - | +| large | - | + + +### TCardStyle +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| topText | - | +| topTextWithBlueBorder | - | +| errorStyle | - | + + \ No newline at end of file diff --git a/tdesign-site/src/link/README.md b/tdesign-site/src/link/README.md index c4d51d609..de56f59f4 100644 --- a/tdesign-site/src/link/README.md +++ b/tdesign-site/src/link/README.md @@ -150,7 +150,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | color | Color? | - | link 文本的颜色,如果不设置则根据状态和风格进行计算 | | fontSize | double? | - | link 文本的字体大小,如果不设置则根据状态和风格进行计算 | | iconSize | double? | - | link icon 大小,如果不设置则根据状态和风格进行计算 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | label | String | - | link 展示的文本 | | leftGapWithIcon | double? | - | 前置icon和文本之间的间隔,如果不设置则根据状态和风格进行计算 | | linkClick | LinkClick? | - | link 被点击之后所采取的动作,会将uri当做参数传入到该方法当中 | @@ -164,4 +164,61 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | uri | Uri? | - | link 跳转的uri | +### TLinkType +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| basic | - | +| withUnderline | - | +| withPrefixIcon | - | +| withSuffixIcon | - | + + +### TLinkStyle +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| primary | - | +| defaultStyle | - | +| danger | - | +| warning | - | +| success | - | + + +### TLinkState +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| normal | - | +| active | - | +| disabled | - | + + +### TLinkSize +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| small | - | +| medium | - | +| large | - | + + +### LinkClick +#### 简介 +限制Function类型,防止传递错误的Function,导致参数对不上 +#### 类型定义 + +```dart +typedef LinkClick = Function(Uri? uri); +``` + + \ No newline at end of file diff --git a/tdesign-site/src/loading/README.md b/tdesign-site/src/loading/README.md index cf203e71c..519d3af70 100644 --- a/tdesign-site/src/loading/README.md +++ b/tdesign-site/src/loading/README.md @@ -260,11 +260,37 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | duration | int | 2000 | 一次刷新的时间,控制动画速度 | | icon | TLoadingIcon? | TLoadingIcon.circle | 图标,支持圆形、点状、菊花状 | | iconColor | Color? | - | 图标颜色 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | refreshWidget | Widget? | - | 失败刷新组件 | | size | TLoadingSize | - | 尺寸 | | text | String? | - | 文案 | | textColor | Color? | - | 文案颜色 | +### TLoadingSize +#### 简介 +Loading 尺寸 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| small | 小尺寸 | +| medium | 中尺寸 | +| large | 大尺寸 | + + +### TLoadingIcon +#### 简介 +Loading图标 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| circle | 圆形 | +| point | 点状 | +| activity | 菊花状 | + + \ No newline at end of file diff --git a/tdesign-site/src/message/README.md b/tdesign-site/src/message/README.md index 176eafef1..5ed4b412f 100644 --- a/tdesign-site/src/message/README.md +++ b/tdesign-site/src/message/README.md @@ -295,7 +295,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | content | String? | - | 通知内容 | | duration | int? | 3000 | 消息内置计时器 | | icon | dynamic | true | 自定义消息前面的图标 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | link | dynamic | - | 链接名称 | | marquee | MessageMarquee? | - | 跑马灯效果 | | offset | List? | - | 相对于 placement 的偏移量 | @@ -308,12 +308,26 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; #### 静态方法 -| 名称 | 返回类型 | 参数 | 说明 | +##### TMessage.showMessage + +返回类型:`void` + +| 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| showMessage | | required BuildContext context, String? content, bool? visible, int? duration, dynamic closeBtn, dynamic icon, dynamic link, MessageMarquee? marquee, List? offset, MessageTheme? theme, VoidCallback? onCloseBtnClick, VoidCallback? onDurationEnd, VoidCallback? onLinkClick, | | +| context | BuildContext | - | - | +| content | String? | - | 通知内容 | +| visible | bool? | - | 是否显示 | +| duration | int? | - | 消息内置计时器 | +| closeBtn | dynamic | - | 关闭按钮 | +| icon | dynamic | - | 自定义消息前面的图标 | +| link | dynamic | - | 链接名称 | +| marquee | MessageMarquee? | - | 跑马灯效果 | +| offset | List? | - | 相对于 placement 的偏移量 | +| theme | MessageTheme? | - | 消息组件风格 info/success/warning/error | +| onCloseBtnClick | VoidCallback? | - | 点击关闭按钮触发 | +| onDurationEnd | VoidCallback? | - | 计时结束后触发 | +| onLinkClick | VoidCallback? | - | 点击链接文本时触发 | -``` -``` ### MessageMarquee #### 默认构造方法 @@ -324,8 +338,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | loop | int? | - | 循环次数 | | speed | int? | - | 速度 | -``` -``` ### MessageLink #### 默认构造方法 @@ -337,4 +349,18 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | uri | Uri? | - | 资源链接 | +### MessageTheme +#### 简介 +定义消息主题枚举 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| info | 普通通知 | +| success | 成功通知 | +| warning | 警示通知 | +| error | 错误通知 | + + \ No newline at end of file diff --git a/tdesign-site/src/navbar/README.md b/tdesign-site/src/navbar/README.md index 10d795948..a92a54be2 100644 --- a/tdesign-site/src/navbar/README.md +++ b/tdesign-site/src/navbar/README.md @@ -303,7 +303,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | centerTitle | bool | true | 标题是否居中 | | flexibleSpace | Widget? | - | 固定背景 | | height | double | 48 | 高度 | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | leftBarItems | List? | - | 左边操作项 | | onBack | VoidCallback? | - | 返回事件 | | opacity | double | 1.0 | 透明度 | @@ -320,8 +320,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | useBorderStyle | bool | false | 是否使用边框模式 | | useDefaultBack | bool | true | 是否使用默认的返回 | -``` -``` ### TNavBarItem #### 默认构造方法 @@ -334,7 +332,15 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | iconColor | Color? | - | 图标颜色 | | iconSize | double? | 24.0 | 图标尺寸 | | iconWidget | Widget? | - | 图标组件,优先级高于 icon | -| padding | EdgeInsetsGeometry? | - | | +| padding | EdgeInsetsGeometry? | - | 内部填充 | + + +### TBarItemAction +#### 类型定义 + +```dart +typedef TBarItemAction = void Function(); +``` \ No newline at end of file diff --git a/tdesign-site/src/notice-bar/README.md b/tdesign-site/src/notice-bar/README.md index f9b95f595..6ad02fea7 100644 --- a/tdesign-site/src/notice-bar/README.md +++ b/tdesign-site/src/notice-bar/README.md @@ -286,8 +286,6 @@ Widget _cardNoticeBar(BuildContext context) { ## API ### TNoticeBar -#### 简介 - #### 默认构造方法 | 参数 | 类型 | 默认值 | 说明 | @@ -297,7 +295,7 @@ Widget _cardNoticeBar(BuildContext context) { | direction | Axis? | Axis.horizontal | 滚动方向 | | height | double | 22 | 文字高度 (当使用prefixIcon或suffixIcon时,icon大小值等于该属性) | | interval | int? | 3000 | 步进滚动间隔时间(毫秒) | -| key | | - | | +| key | Key? | - | 组件标识,用于区分或保留组件状态。 | | left | Widget? | - | 左侧内容(自定义左侧内容,优先级高于prefixIcon) | | marquee | bool? | false | 跑马灯效果 | | maxLines | int? | 1 | 文本行数(仅静态有效) | @@ -309,8 +307,6 @@ Widget _cardNoticeBar(BuildContext context) { | suffixIcon | IconData? | - | 右侧图标 | | theme | TNoticeBarTheme? | TNoticeBarTheme.info | 主题 | -``` -``` ### TNoticeBarStyle #### 简介 @@ -329,9 +325,41 @@ Widget _cardNoticeBar(BuildContext context) { #### 工厂构造方法 -| 名称 | 说明 | -| --- | --- | -| TNoticeBarStyle.generateTheme | 根据主题生成样式 | +##### TNoticeBarStyle.generateTheme + +根据主题生成样式 + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| context | BuildContext | - | 上下文 | +| theme | TNoticeBarTheme? | TNoticeBarTheme.info | - | + + +### TNoticeBarType +#### 简介 +公告栏类型 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| none | 静止(默认) | +| scroll | 滚动 | +| step | 步进 | + + +### TNoticeBarTheme +#### 简介 +公告栏主题 +#### 枚举值 + + +| 名称 | 说明 | +| --- | --- | +| info | 信息(默认) | +| success | 成功 | +| warning | 警告 | +| error | 错误 | \ No newline at end of file diff --git a/tdesign-site/src/picker/README.md b/tdesign-site/src/picker/README.md index 2dfa16264..f061a540b 100644 --- a/tdesign-site/src/picker/README.md +++ b/tdesign-site/src/picker/README.md @@ -30,11 +30,13 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('选中城市: ${selectedCity.isEmpty ? "未选择" : selectedCity}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary)), const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: cityItems, + child: TPicker( + items: cityItems, onChange: (v) => setState(() => selectedCity = v.labels.first)), ), ], @@ -54,13 +56,16 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('选中时间: ${selectedTime.isEmpty ? "未选择" : selectedTime}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary)), const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: timeItems, itemCount: 5, - onChange: (v) => setState(() => - selectedTime = '${v.values[0]}:${v.values[1].toString().padLeft(2, '0')}:${v.values[2].toString().padLeft(2, '0')}')), + child: TPicker( + items: timeItems, + itemCount: 5, + onChange: (v) => setState(() => selectedTime = + '${v.values[0]}:${v.values[1].toString().padLeft(2, '0')}:${v.values[2].toString().padLeft(2, '0')}')), ), ], ); @@ -79,12 +84,16 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('选中地区: ${selectedLinked.isEmpty ? "未选择" : selectedLinked}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary)), const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: linkedItems, initialValue: const ['GD', 'SZ', 'NS'], - onChange: (v) => setState(() => selectedLinked = v.labels.join(' / '))), + child: TPicker( + items: linkedItems, + initialValue: const ['GD', 'SZ', 'NS'], + onChange: (v) => + setState(() => selectedLinked = v.labels.join(' / '))), ), ], ); @@ -178,15 +187,20 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('选中: ${selectedItemDisabled.isEmpty ? "未选择" : selectedItemDisabled}', - style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary)), + Text( + '选中: ${selectedItemDisabled.isEmpty ? "未选择" : selectedItemDisabled}', + style: TextStyle( + fontSize: 14, color: TTheme.of(context).textColorSecondary)), const SizedBox(height: 4), Text('提示: 标灰的选项不可选(第1列「保密」、第2列「A排1座/A排6座/A排7座/A排8座/A排12座」)', - style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), + style: TextStyle( + fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: itemDisabledItems, initialValue: const ['M', 'A5'], + child: TPicker( + items: itemDisabledItems, + initialValue: const ['M', 'A5'], onChange: (v) => setState(() => selectedItemDisabled = '${v.labels.first} ${v.labels.last}')), ), @@ -224,13 +238,16 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; const SizedBox(height: 8), _pickerCard( context, - child: TPicker(items: cityItems, initialValue: const ['GZ'], + child: TPicker( + items: cityItems, + initialValue: const ['GZ'], onChange: (v) => debugPrint('选中: $v'), disabled: globalDisabled), ), const SizedBox(height: 4), Text('切换开关可控制整个选择器的禁用/启用状态', - style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), + style: TextStyle( + fontSize: 12, color: TTheme.of(context).textColorPlaceholder)), ], ); }
@@ -327,8 +344,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; titleWidget: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(TIcons.location, - size: 18, color: theme.brandNormalColor), + Icon(TIcons.location, size: 18, color: theme.brandNormalColor), const SizedBox(width: 4), Text('选择地区', style: TextStyle( @@ -339,7 +355,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; ], ), cancel: Icon(TIcons.close, size: 22, color: theme.fontGyColor2), - confirm: Icon(TIcons.check, size: 22, color: theme.brandNormalColor), + confirm: + Icon(TIcons.check, size: 22, color: theme.brandNormalColor), ), ), ], @@ -357,19 +374,22 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
   Widget buildCustomKeys(BuildContext context) {
     // 用 keys 告诉组件「city 映射为 label,code 是 value,readonly 是 disabled」
-    const keys = TPickerKeys(label: 'city', value: 'code', disabled: 'readonly');
+    const keys =
+        TPickerKeys(label: 'city', value: 'code', disabled: 'readonly');
     final label = _customKeysValue?.labels.join() ?? '';
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
         Text(
           '后端原始字段:city / code / readonly。通过 keys(label: "city") 映射为 label',
-          style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder),
+          style: TextStyle(
+              fontSize: 12, color: TTheme.of(context).textColorPlaceholder),
         ),
         const SizedBox(height: 4),
         Text(
           '当前选中:${label.isEmpty ? "未选择" : label}',
-          style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary),
+          style: TextStyle(
+              fontSize: 14, color: TTheme.of(context).textColorSecondary),
         ),
         const SizedBox(height: 8),
         _pickerCard(
@@ -399,7 +419,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
       children: [
         Text(
           '示例:height(300) + itemCount(7),每屏显示 7 项',
-          style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder),
+          style: TextStyle(
+              fontSize: 12, color: TTheme.of(context).textColorPlaceholder),
         ),
         const SizedBox(height: 8),
         _pickerCard(
@@ -429,12 +450,14 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
       children: [
         Text(
           '示例:itemBuilder 自定义子项渲染,可添加图标、背景色等',
-          style: TextStyle(fontSize: 12, color: TTheme.of(context).textColorPlaceholder),
+          style: TextStyle(
+              fontSize: 12, color: TTheme.of(context).textColorPlaceholder),
         ),
         const SizedBox(height: 4),
         Text(
           '选中: ${_customItemBuilderValue.isEmpty ? "未选择" : _customItemBuilderValue}',
-          style: TextStyle(fontSize: 14, color: TTheme.of(context).textColorSecondary),
+          style: TextStyle(
+              fontSize: 14, color: TTheme.of(context).textColorSecondary),
         ),
         const SizedBox(height: 8),
         _pickerCard(
@@ -460,15 +483,19 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
                       content,
                       style: TextStyle(
                         fontSize: 16,
-                        fontWeight: selected ? FontWeight.w600 : FontWeight.normal,
-                        color: selected ? theme.brandNormalColor : theme.fontGyColor1,
+                        fontWeight:
+                            selected ? FontWeight.w600 : FontWeight.normal,
+                        color: selected
+                            ? theme.brandNormalColor
+                            : theme.fontGyColor1,
                       ),
                     ),
                   ],
                 ),
               );
             },
-            onChange: (v) => setState(() => _customItemBuilderValue = v.labels.first),
+            onChange: (v) =>
+                setState(() => _customItemBuilderValue = v.labels.first),
           ),
         ),
       ],
@@ -485,25 +512,23 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| cancel | Widget? | - | 工具栏左侧自定义插槽,默认使用 [TResourceDelegate.cancel] |
-| confirm | Widget? | - | 工具栏右侧自定义插槽,默认使用 [TResourceDelegate.confirm] |
+| cancel | Widget? | - | 工具栏左侧自定义插槽,默认使用 [TResourceDelegate.cancel] 可用于渲染图标、图标+文字组合等。点击事件依然由外层 [GestureDetector] 处理,触发 [onCancel] 回调——所以插槽内的 Widget 不需要自己处理点击。 ```dart // 简单改文字 TPicker( cancel: const Text('关闭'), onCancel: () => Navigator.of(context).pop(), ) // 带图标 TPicker( cancel: const Icon(Icons.close, size: 22), onCancel: () => Navigator.of(context).pop(), ) ``` |
+| confirm | Widget? | - | 工具栏右侧自定义插槽,默认使用 [TResourceDelegate.confirm] 可用于渲染图标、图标+文字组合等。点击事件依然由外层 [GestureDetector] 处理,触发 [onConfirm] 回调——所以插槽内的 Widget 不需要自己处理点击。 ```dart // 简单改文字 TPicker( confirm: const Text('确定'), onConfirm: (v) => Navigator.of(context).pop(v), ) // 带图标 TPicker( confirm: const Icon(Icons.check, size: 22), onConfirm: (v) => Navigator.of(context).pop(v), ) ``` |
 | disabled | bool | false | 是否禁用整个选择器(禁止滚动和操作),默认 false |
 | height | double | 200 | 视窗高度,默认 200 |
 | initialValue | List? | - | 初始选中值列表(按 value 匹配) |
 | itemBuilder | ItemBuilderType? | - | 自定义子项构建器(disabled 项仍由内部统一渲染,不会走此 builder) |
 | itemCount | int | 5 | 每屏显示 item 数,默认 5 |
 | itemDistanceCalculator | ItemDistanceCalculator? | - | 自定义距离计算器(控制颜色/字重/字号随"离中心距离"的变化) |
-| items | TPickerItems | - | 数据源(必填) |
-| key |  | - |  |
-| onCancel | VoidCallback? | - | 点击「取消」按钮回调 |
-| onChange | void Function(TPickerValue)? | - | 值改变回调(滚动时实时触发) |
-| onConfirm | void Function(TPickerValue)? | - | 点击「确认」按钮回调 |
-| onLoad | void Function(TPickerLoadEvent)? | - | 列选中项变化的事件回调 |
-| title | String? | - | 工具栏中部标题(可选,不传时中部留白) |
-| titleWidget | Widget? | - | 工具栏中部自定义标题插槽 |
+| items | TPickerItems | - | 数据源(必填) 使用密封类 [TPickerItems] 编译期强制二选一: - [TPickerColumns] → 多列独立选择 - [TPickerLinked] → 联动选择 自由结构数据通过 `.fromRaw()` 工厂构造归一化。 |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
+| onCancel | VoidCallback? | - | 点击「取消」按钮回调 仅作为点击事件通知,不携带任何参数。组件本身不会做任何 popup 操作,业务层可在此自行决定是否关闭弹窗、重置状态等。 |
+| onChange | void Function(TPickerValue)? | - | 值改变回调(滚动时实时触发) 触发时机: - 用户滚动经过某个 enabled 项并稳定时 - disabled 修正动画完成后,回调最终落点 **注意**:此回调代表"滚动时实时变化",不代表"用户已确认选择"。 如需"已确认"语义,请使用 [onConfirm]。 如需做网络请求/埋点等去抖处理,请在业务层自行 debounce。 |
+| onConfirm | void Function(TPickerValue)? | - | 点击「确认」按钮回调 携带当前选中的完整 [TPickerValue],包含: - `selectedOptions`: 当前选中的所有 [TPickerOption] - `values`: 各列选中项的 value 列表 - `labels`: 各列选中项的 label 列表 - `indexes`: 各列选中项的索引 与 [onChange] 不同——只有用户点击「确认」时才触发,代表"已确认选择"。 组件本身不会做任何 popup 操作,业务层可在此自行决定是否关闭弹窗、 提交表单等。 |
+| onLoad | void Function(TPickerLoadEvent)? | - | 列选中项变化的事件回调 **触发时机**:每次用户滚动到一个 enabled 项后都会触发(联动模式下还会 在新展开的列就位后触发)。组件本身不做"距底部多少项"的阈值判断——把 决策权交给业务层。 **事件参数**包含: - [TPickerLoadEvent.column]:触发列索引 - [TPickerLoadEvent.remaining]:当前列距底部剩余项数 - [TPickerLoadEvent.displayedCount]:当前列总项数 - [TPickerLoadEvent.parentValue]:联动模式下父级选中值(首列为 null) **典型用法**:业务层根据 [TPickerLoadEvent.remaining] 自行判断是否加载更多。 ```dart onLoad: (e) async { if (e.remaining > 5 \|\| _isLoading) return; // 距底部还远 / 已在加载,跳过 _isLoading = true; final more = await fetchMore(parent: e.parentValue); setState(() { _data.addAll(more); _isLoading = false; }); } ``` |
+| title | String? | - | 工具栏中部标题(可选,不传时中部留白) 顶部工具栏永远显示,包含「取消」「标题」「确认」三块。 用户点击「取消」触发 [onCancel],点击「确认」触发 [onConfirm]。 选择器与弹窗(popup)完全解耦——关闭/打开弹窗的逻辑由业务层在 这两个回调中自行控制。 典型用法(与 popup 弹窗组合): ```dart TPicker( items: items, title: '请选择地区', onCancel: () => setState(() => visible = false), onConfirm: (value) { setState(() { selected = value; visible = false; }); }, ) ``` |
+| titleWidget | Widget? | - | 工具栏中部自定义标题插槽 传入后会**完全替换**默认的 [title] 文字,可用于渲染更复杂的标题(副标题、图标+文字等)。 标题区域不响应点击。 |
 
-```
-```
 
 ### TPickerOption
 #### 默认构造方法
@@ -514,8 +539,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | label | String | - | 展示文字(可包含 emoji、单位、国际化等) |
 | value | dynamic | - | 业务值(onChange 回调返回此字段) |
 
-```
-```
 
 ### TPickerValue
 #### 默认构造方法
@@ -525,8 +548,13 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | indexes | List | - | 每列选中项的索引 |
 | selectedOptions | List | - | 每列选中的完整 option |
 
-```
-```
+#### 公开属性
+
+| 属性 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| labels | List | - | 所有选中项的 label(展示用) 顺序与列顺序对应,可直接用于 UI 展示。 懒计算并缓存,生命周期内只计算一次。 |
+| values | List | - | 所有选中项的 value(提交表单用) 顺序与列顺序对应,可直接用于表单提交。 懒计算并缓存,生命周期内只计算一次。 |
+
 
 ### TPickerLoadEvent
 #### 默认构造方法
@@ -535,63 +563,71 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | --- | --- | --- | --- |
 | column | int | - | 触发事件的列索引(0 表示第一列) |
 | displayedCount | int | - | 当前列已展示的选项总数 |
-| parentValue | dynamic | - | 当前列的父级选中值(联动模式下使用) |
+| parentValue | dynamic | - | 当前列的父级选中值(联动模式下使用) 第一列时为 null;业务层可用此值从原始数据中筛选子级选项。 |
 | remaining | int | - | 距底部剩余的选项数(业务可用此值做"接近底部时加载"判断) |
 
-```
-```
 
 ### TPickerColumns
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| columns | List> | columns | 每列的选项列表 |
+| columns | List> | - | 每列的选项列表 |
 
 
 #### 工厂构造方法
 
-| 名称  | 说明 |
-| --- |  --- |
-| TPickerColumns.fromRaw  | 从自由结构的 raw 数据创建,自动归一化
+##### TPickerColumns.fromRaw
+
+从自由结构的 raw 数据创建,自动归一化
 
  ```dart
  TPickerColumns.fromRaw(
    [['北京', '上海', '广州']],
    keys: const TPickerKeys(label: 'name', value: 'code'),
  )
- ``` |
+ ```
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| rawColumns | List | - | - |
+| keys | TPickerKeys | TPickerKeys.defaults | - |
 
-```
-```
 
 ### TPickerLinked
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| tree | Map | tree | 联动树结构:`Map` |
+| tree | Map | - | 联动树结构:`Map` value 可以是: - `Map` → 下一级联动 - `List` → 叶子级选项 |
 
 
 #### 工厂构造方法
 
-| 名称  | 说明 |
-| --- |  --- |
-| TPickerLinked.fromRaw  | 从自由结构的 raw Map 数据创建,自动归一化
+##### TPickerLinked.fromRaw
+
+从自由结构的 raw Map 数据创建,自动归一化
 
  ```dart
  TPickerLinked.fromRaw({
    '广东': {'深圳': ['南山', '福田'], '广州': ['天河']},
    '浙江': {'杭州': ['西湖']},
  })
- ``` |
+ ```
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| rawTree | Map | - | - |
+| keys | TPickerKeys | TPickerKeys.defaults | - |
 
-```
-```
 
 ### TPickerItems
-```
-```
+#### 简介
+选择器数据源密封类
+
+ 编译期强制二选一,消除运行时类型错误:
+ - [TPickerColumns] → 多列独立选择
+ - [TPickerLinked] → 联动选择
 
 ### TPickerKeys
 #### 默认构造方法
@@ -603,5 +639,11 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | label | String | 'label' | 展示文案对应的字段名,默认 `label` |
 | value | String | 'value' | 业务值对应的字段名,默认 `value` |
 
+#### 静态成员
+
+| 名称 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| defaults | TPickerKeys | - | 默认配置(`label / value / disabled / children`) |
+
 
   
\ No newline at end of file
diff --git a/tdesign-site/src/popover/README.md b/tdesign-site/src/popover/README.md
index 60e8283db..aae1a64a0 100644
--- a/tdesign-site/src/popover/README.md
+++ b/tdesign-site/src/popover/README.md
@@ -1276,21 +1276,34 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 ## API
 ### TPopover
-#### 简介
-
 
 #### 静态方法
 
-| 名称 | 返回类型 | 参数 | 说明 |
+##### TPopover.showPopover
+
+返回类型:`Future`
+
+| 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| showPopover |  |   required BuildContext context,  String? content,  Widget? contentWidget,  double offset,  TPopoverTheme? theme,  bool closeOnClickOutside,  TPopoverPlacement? placement,  bool? showArrow,  double arrowSize,  EdgeInsetsGeometry? padding,  double? width,  double? height,  Color? overlayColor,  OnTap? onTap,  OnLongTap? onLongTap,  BorderRadius? radius, |  |
+| context | BuildContext | - | 上下文 |
+| content | String? | - | 显示内容 |
+| contentWidget | Widget? | - | 自定义内容 |
+| offset | double | 4 | 偏移 |
+| theme | TPopoverTheme? | - | 弹出气泡主题 |
+| closeOnClickOutside | bool | true | - |
+| placement | TPopoverPlacement? | - | 浮层出现位置 |
+| showArrow | bool? | true | 是否显示浮层箭头 |
+| arrowSize | double | 8 | 箭头大小 |
+| padding | EdgeInsetsGeometry? | - | 内容内边距 |
+| width | double? | - | 内容宽度(包含padding,实际高度:height - paddingLeft - paddingRight) |
+| height | double? | - | 内容高度(包含padding,实际高度:height - paddingTop - paddingBottom) |
+| overlayColor | Color? | Colors.transparent | - |
+| onTap | OnTap? | - | 点击事件 |
+| onLongTap | OnLongTap? | - | 长按事件 |
+| radius | BorderRadius? | - | 圆角 |
 
-```
-```
 
 ### TPopoverWidget
-#### 简介
-
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -1300,7 +1313,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | contentWidget | Widget? | - | 自定义内容 |
 | context | BuildContext | - | 上下文 |
 | height | double? | - | 内容高度(包含padding,实际高度:height - paddingTop - paddingBottom) |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | offset | double | 4 | 偏移 |
 | onLongTap | OnLongTap? | - | 长按事件 |
 | onTap | OnTap? | - | 点击事件 |
@@ -1312,4 +1325,54 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | width | double? | - | 内容宽度(包含padding,实际高度:height - paddingLeft - paddingRight) |
 
 
+### TPopoverTheme
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| dark | 暗色 |
+| light | 亮色 |
+| info | 品牌色 |
+| success | 成功 |
+| warning | 警告 |
+| error | 错误 |
+
+
+### TPopoverPlacement
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| topLeft | 上左 |
+| top | 上 |
+| topRight | 上右 |
+| rightTop | 右上 |
+| right | 右 |
+| rightBottom | 右下 |
+| bottomRight | 下右 |
+| bottom | 下 |
+| bottomLeft | 下左 |
+| leftBottom | 左下 |
+| left | 左 |
+| leftTop | 左上 |
+
+
+### OnTap
+#### 类型定义
+
+```dart
+typedef OnTap =  Function(String? content);
+```
+
+
+### OnLongTap
+#### 类型定义
+
+```dart
+typedef OnLongTap =  Function(String? content);
+```
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/popup/README.md b/tdesign-site/src/popup/README.md
index 9c220755c..b9588e4f2 100644
--- a/tdesign-site/src/popup/README.md
+++ b/tdesign-site/src/popup/README.md
@@ -33,21 +33,16 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
       type: TButtonType.outline,
       size: TButtonSize.large,
       onTap: () {
-        Navigator.of(context).push(
-          TSlidePopupRoute(
-              slideTransitionFrom: SlideTransitionFrom.top,
-              open: () {
-                print('open');
-              },
-              opened: () {
-                print('opened');
-              },
-              builder: (context) {
-                return Container(
-                  color: TTheme.of(context).bgColorContainer,
-                  height: 240,
-                );
-              }),
+        TPopup.show(
+          context,
+          options: TPopupOptions.top(
+              height: 240,
+              onOpen: () => print('open'),
+              onOpened: () => print('opened'),
+              child: Container(
+                color: TTheme.of(context).bgColorContainer,
+                height: 240,
+              )),
         );
       },
     );
@@ -69,15 +64,13 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
       type: TButtonType.outline,
       size: TButtonSize.large,
       onTap: () {
-        Navigator.of(context).push(
-          TSlidePopupRoute(
-              slideTransitionFrom: SlideTransitionFrom.left,
-              builder: (context) {
-                return Container(
-                  color: TTheme.of(context).bgColorContainer,
-                  width: 280,
-                );
-              }),
+        TPopup.show(
+          context,
+          options: TPopupOptions.left(
+              width: 280,
+              child: Container(
+                color: TTheme.of(context).bgColorContainer,
+              )),
         );
       },
     );
@@ -99,20 +92,19 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
       type: TButtonType.outline,
       size: TButtonSize.large,
       onTap: () {
-        Navigator.of(context).push(
-          TSlidePopupRoute(
-              slideTransitionFrom: SlideTransitionFrom.center,
-              builder: (context) {
-                return Container(
-                  decoration: BoxDecoration(
-                    color: TTheme.of(context).bgColorContainer,
-                    borderRadius:
-                        BorderRadius.circular(TTheme.of(context).radiusLarge),
-                  ),
-                  width: 240,
-                  height: 240,
-                );
-              }),
+        TPopup.show(
+          context,
+          options: TPopupOptions.center(
+              closeBuilder: null,
+              child: Container(
+                decoration: BoxDecoration(
+                  color: TTheme.of(context).bgColorContainer,
+                  borderRadius:
+                      BorderRadius.circular(TTheme.of(context).radiusLarge),
+                ),
+                width: 240,
+                height: 240,
+              )),
         );
       },
     );
@@ -134,15 +126,15 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
       type: TButtonType.outline,
       size: TButtonSize.large,
       onTap: () {
-        Navigator.of(context).push(
-          TSlidePopupRoute(
-              slideTransitionFrom: SlideTransitionFrom.bottom,
-              builder: (context) {
-                return Container(
-                  color: TTheme.of(context).bgColorContainer,
-                  height: 240,
-                );
-              }),
+        TPopup.show(
+          context,
+          options: TPopupOptions.bottom(
+              height: 240,
+              headerBuilder: null,
+              child: Container(
+                color: TTheme.of(context).bgColorContainer,
+                height: 240,
+              )),
         );
       },
     );
@@ -164,15 +156,13 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
       type: TButtonType.outline,
       size: TButtonSize.large,
       onTap: () {
-        Navigator.of(context).push(
-          TSlidePopupRoute(
-              slideTransitionFrom: SlideTransitionFrom.right,
-              builder: (context) {
-                return Container(
-                  color: TTheme.of(context).bgColorContainer,
-                  width: 280,
-                );
-              }),
+        TPopup.show(
+          context,
+          options: TPopupOptions.right(
+              width: 280,
+              child: Container(
+                color: TTheme.of(context).bgColorContainer,
+              )),
         );
       },
     );
@@ -195,23 +185,12 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
       type: TButtonType.outline,
       size: TButtonSize.large,
       onTap: () {
-        Navigator.of(context).push(
-          TSlidePopupRoute(
-            slideTransitionFrom: SlideTransitionFrom.bottom,
-            builder: (context) {
-              return TPopupBottomConfirmPanel(
-                title: '标题文字',
-                leftClick: () {
-                  Navigator.maybePop(context);
-                },
-                rightClick: () {
-                  TToast.showText('确定', context: context);
-                  Navigator.maybePop(context);
-                },
-                child: Container(height: 200),
-              );
-            },
-          ),
+        TPopup.show(
+          context,
+          options: TPopupOptions.bottom(
+              height: 280,
+              titleWidget: TText('标题文字'),
+              child: Container(height: 200)),
         );
       },
     );
@@ -225,31 +204,58 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
   
-  Widget _buildPopFromBottomWithOperation(BuildContext context) {
+  Widget _buildPopFromBottomWithCloseAndTitle(BuildContext context) {
     return TButton(
-      text: '底部弹出层-带操作',
+      text: '底部弹出层-带标题及关闭',
       isBlock: true,
       theme: TButtonTheme.primary,
       type: TButtonType.outline,
       size: TButtonSize.large,
       onTap: () {
-        Navigator.of(context).push(TSlidePopupRoute(
-            modalBarrierColor: TTheme.of(context).fontGyColor2,
-            slideTransitionFrom: SlideTransitionFrom.bottom,
-            builder: (context) {
-              return TPopupBottomConfirmPanel(
-                leftClick: () {
-                  Navigator.maybePop(context);
-                },
-                rightClick: () {
-                  TToast.showText('确定', context: context);
-                  Navigator.maybePop(context);
-                },
-                child: Container(
-                  height: 200,
-                ),
-              );
-            }));
+        TPopup.show(
+          context,
+          options: TPopupOptions.bottom(
+              height: 280,
+              cancelBuilder: (_, close) => GestureDetector(
+                    behavior: HitTestBehavior.opaque,
+                    onTap: close,
+                    child: Padding(
+                      padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
+                      child: TText(
+                        '关闭',
+                        textColor: TTheme.of(context).textColorSecondary,
+                        font: TTheme.of(context).fontBodyLarge,
+                      ),
+                    ),
+                  ),
+              titleWidget: Row(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  Icon(TIcons.info_circle,
+                      color: TTheme.of(context).brandNormalColor, size: 18),
+                  const SizedBox(width: 4),
+                  TText(
+                    '自定义标题',
+                    textColor: TTheme.of(context).brandNormalColor,
+                    font: TTheme.of(context).fontTitleMedium,
+                  ),
+                ],
+              ),
+              confirmBuilder: (_, close) => GestureDetector(
+                    behavior: HitTestBehavior.opaque,
+                    onTap: close,
+                    child: Padding(
+                      padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
+                      child: TText(
+                        '完成',
+                        textColor: TTheme.of(context).brandNormalColor,
+                        font: TTheme.of(context).fontTitleMedium,
+                        fontWeight: FontWeight.w600,
+                      ),
+                    ),
+                  ),
+              child: Container(height: 200)),
+        );
       },
     );
   }
@@ -262,26 +268,29 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
-  Widget _buildPopFromBottomWithCloseAndTitle(BuildContext context) {
+  Widget _buildPopFromCenterWithClose(BuildContext context) {
     return TButton(
-      text: '底部弹出层-带标题及关闭',
+      text: '居中弹出层-带关闭',
       isBlock: true,
       theme: TButtonTheme.primary,
       type: TButtonType.outline,
       size: TButtonSize.large,
       onTap: () {
-        Navigator.of(context).push(
-          TSlidePopupRoute(
-              slideTransitionFrom: SlideTransitionFrom.bottom,
-              builder: (context) {
-                return TPopupBottomDisplayPanel(
-                  title: '标题文字',
-                  closeClick: () {
-                    Navigator.maybePop(context);
-                  },
-                  child: Container(height: 200),
-                );
-              }),
+        TPopup.show(
+          context,
+          options: TPopupOptions.center(
+              closeOnOverlayClick: false,
+              width: 240,
+              height: 240,
+              closeBuilder: (_, close) => IconButton(
+                    icon: Icon(
+                      TIcons.close_circle,
+                      color: TTheme.of(context).fontWhColor1,
+                      size: 32,
+                    ),
+                    onPressed: close,
+                  ),
+              child: const SizedBox(width: 240, height: 240)),
         );
       },
     );
@@ -295,27 +304,33 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
   
-  Widget _buildPopFromBottomWithCloseAndLeftTitle(BuildContext context) {
+  Widget _buildPopFromCenterWithUnderClose(BuildContext context) {
     return TButton(
-      text: '底部弹出层-带左边标题及关闭',
+      text: '居中弹出层-自定义下方按钮',
       isBlock: true,
       theme: TButtonTheme.primary,
       type: TButtonType.outline,
       size: TButtonSize.large,
       onTap: () {
-        Navigator.of(context).push(
-          TSlidePopupRoute(
-              slideTransitionFrom: SlideTransitionFrom.bottom,
-              builder: (context) {
-                return TPopupBottomDisplayPanel(
-                  title: '标题文字',
-                  titleLeft: true,
-                  closeClick: () {
-                    Navigator.maybePop(context);
-                  },
-                  child: Container(height: 200),
-                );
-              }),
+        TPopup.show(
+          context,
+          options: TPopupOptions.center(
+              closeOnOverlayClick: true,
+              width: 240,
+              height: 200,
+              closeBuilder: (_, close) => IconButton(
+                    icon: Icon(
+                      TIcons.poweroff,
+                      color: TTheme.of(context).fontWhColor1,
+                      size: 36,
+                    ),
+                    onPressed: close,
+                  ),
+              child: Container(
+                width: 240,
+                height: 200,
+                color: TTheme.of(context).bgColorContainer,
+              )),
         );
       },
     );
@@ -329,25 +344,65 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
   
-  Widget _buildPopFromBottomWithClose(BuildContext context) {
+  Widget _buildNestedPopup(BuildContext context) {
     return TButton(
-      text: '底部弹出层-带关闭',
+      text: '内层再弹一层(嵌套叠加)',
       isBlock: true,
       theme: TButtonTheme.primary,
       type: TButtonType.outline,
       size: TButtonSize.large,
       onTap: () {
-        Navigator.of(context).push(
-          TSlidePopupRoute(
-              slideTransitionFrom: SlideTransitionFrom.bottom,
-              builder: (context) {
-                return TPopupBottomDisplayPanel(
-                  closeClick: () {
-                    Navigator.maybePop(context);
-                  },
-                  child: Container(height: 200),
-                );
-              }),
+        TPopupHandle? outerHandle;
+        outerHandle = TPopup.show(
+          context,
+          options: TPopupOptions.bottom(
+              height: 360,
+              headerBuilder: null,
+              child: Builder(
+                builder: (innerContext) {
+                  return Padding(
+                    padding: const EdgeInsets.all(16),
+                    child: Column(
+                      crossAxisAlignment: CrossAxisAlignment.stretch,
+                      children: [
+                        TText(
+                          '外层:headerBuilder: null,仅 child',
+                          textColor: TTheme.of(innerContext).textColorSecondary,
+                        ),
+                        const SizedBox(height: 16),
+                        TButton(
+                          text: '打开内层 Popup',
+                          isBlock: true,
+                          theme: TButtonTheme.primary,
+                          size: TButtonSize.large,
+                          onTap: () {
+                            TPopup.show(
+                              innerContext,
+                              options: TPopupOptions.bottom(
+                                height: 280,
+                                titleWidget: const TText('内层标题'),
+                                child: Container(
+                                  height: 160,
+                                  color: TTheme.of(innerContext)
+                                      .bgColorSecondaryContainer,
+                                ),
+                              ),
+                            );
+                          },
+                        ),
+                        const SizedBox(height: 12),
+                        TButton(
+                          text: '关闭外层',
+                          isBlock: true,
+                          type: TButtonType.outline,
+                          size: TButtonSize.large,
+                          onTap: () => outerHandle?.close(),
+                        ),
+                      ],
+                    ),
+                  );
+                },
+              )),
         );
       },
     );
@@ -355,33 +410,31 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
                                   
+### 1 更多 API
 
 
             
 
 
   
-  Widget _buildPopFromBottomWithTitle(BuildContext context) {
+  Widget _buildApiInset(BuildContext context) {
     return TButton(
-      text: '底部弹出层-仅标题',
+      text: 'bottom inset.left/right',
       isBlock: true,
       theme: TButtonTheme.primary,
       type: TButtonType.outline,
       size: TButtonSize.large,
       onTap: () {
-        Navigator.of(context).push(
-          TSlidePopupRoute(
-              slideTransitionFrom: SlideTransitionFrom.bottom,
-              builder: (context) {
-                return TPopupBottomDisplayPanel(
-                  title: '标题文字',
-                  hideClose: true,
-                  // closeClick: () {
-                  //   Navigator.maybePop(context);
-                  // },
-                  child: Container(height: 200),
-                );
-              }),
+        TPopup.show(
+          context,
+          options: TPopupOptions.bottom(
+              height: 320,
+              inset: const TPopupBottomInset(left: 16, right: 16),
+              titleWidget: TText('左右留白'),
+              child: Container(
+                height: 240,
+                color: TTheme.of(context).bgColorContainer,
+              )),
         );
       },
     );
@@ -395,26 +448,26 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
   
-  Widget _buildPopFromCenterWithClose(BuildContext context) {
+  Widget _buildApiShowOverlayFalse(BuildContext context) {
     return TButton(
-      text: '居中弹出层-带关闭',
+      text: 'showOverlay: false(透明模态)',
       isBlock: true,
       theme: TButtonTheme.primary,
       type: TButtonType.outline,
       size: TButtonSize.large,
       onTap: () {
-        Navigator.of(context).push(
-          TSlidePopupRoute(
-              isDismissible: false,
-              slideTransitionFrom: SlideTransitionFrom.center,
-              builder: (context) {
-                return TPopupCenterPanel(
-                  closeClick: () {
-                    Navigator.maybePop(context);
-                  },
-                  child: const SizedBox(width: 240, height: 240),
-                );
-              }),
+        TPopup.show(
+          context,
+          options: TPopupOptions.bottom(
+              height: 280,
+              showOverlay: false,
+              modal: true,
+              // 不显示可见蒙层,但仍阻断背景交互;须保留其它关闭入口。
+              titleWidget: const TText('透明模态'),
+              child: Container(
+                height: 200,
+                color: TTheme.of(context).bgColorContainer,
+              )),
         );
       },
     );
@@ -428,27 +481,53 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
   
-  Widget _buildPopFromCenterWithUnderClose(BuildContext context) {
+  Widget _buildApiOnOverlayClick(BuildContext context) {
+    return TButton(
+      text: 'onOverlayClick',
+      isBlock: true,
+      theme: TButtonTheme.primary,
+      type: TButtonType.outline,
+      size: TButtonSize.large,
+      onTap: () {
+        TPopup.show(
+          context,
+          options: TPopupOptions.bottom(
+              height: 260,
+              onOverlayClick: () => TToast.showText('点击蒙层', context: context),
+              child: Container(
+                height: 200,
+                color: TTheme.of(context).bgColorContainer,
+              )),
+        );
+      },
+    );
+  }
+ +
+ + + + + + +
+  Widget _buildApiDuration(BuildContext context) {
     return TButton(
-      text: '居中弹出层-关闭在下方',
+      text: 'animationDuration: 600ms',
       isBlock: true,
       theme: TButtonTheme.primary,
       type: TButtonType.outline,
       size: TButtonSize.large,
       onTap: () {
-        Navigator.of(context).push(
-          TSlidePopupRoute(
-              isDismissible: false,
-              slideTransitionFrom: SlideTransitionFrom.center,
-              builder: (context) {
-                return TPopupCenterPanel(
-                  closeUnderBottom: true,
-                  closeClick: () {
-                    Navigator.maybePop(context);
-                  },
-                  child: const SizedBox(width: 240, height: 240),
-                );
-              }),
+        TPopup.show(
+          context,
+          options: TPopupOptions.bottom(
+              height: 240,
+              animationDuration: const Duration(milliseconds: 600),
+              child: Container(
+                height: 200,
+                color: TTheme.of(context).bgColorContainer,
+              )),
         );
       },
     );
@@ -459,103 +538,348 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ## API
-### TSlidePopupRoute
+### TPopup
 #### 简介
-从屏幕的某个方向滑动弹出的Dialog框的路由,比如从顶部、底部、左、右滑出页面
-#### 默认构造方法
+弹出层入口:五向滑入 / 居中弹出,支持蒙层、bottom 操作区、center 面板外下方关闭区。
+
+ 通过 [show] 命令式打开;返回 [TPopupHandle] 用于关闭与再次打开。
+ 多次调用 [show] 会继续压入新的浮层路由,可用于叠加展示。
+
+ **示例**
+
+ ```dart
+ final handle = TPopup.show(
+   context,
+   options: TPopupOptions.bottom(
+     titleWidget: const Text('标题'),
+     child: MyPanel(),
+   ),
+ );
+ handle.close();
+ handle.open();
+ ```
+
+ 配置项见 [TPopupOptions];方向见 [TPopupPlacement]。
+
+#### 工厂构造方法
+
+##### TPopup._
+
+#### 静态方法
+
+##### TPopup.show
+
+打开浮层并压入独立 [PopupRoute]。
+
+ [context] 用于查找 [Navigator] 并展示浮层。
+
+ [options] 浮层配置;方向固定时推荐 [TPopupOptions.bottom] 等命名工厂。
+
+ 返回 [TPopupHandle],可用 [TPopupHandle.close]、[TPopupHandle.open]、
+ [TPopupHandle.isShowing] 控制与查询。
+ 重复调用会继续 push 新的浮层;若需互斥请在业务层管理。
+
+ [navigatorContext] 可选,指定承载浮层的 [Navigator] 的 context,默认 [context]。
+
+ [useRootNavigator] 为 true 时使用根 [Navigator](嵌套导航场景)。
+
+返回类型:`TPopupHandle`
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| barrierClick | VoidCallback? | - | 蒙层点击事件,仅在[modalBarrierFull]为false时触发 |
-| barrierLabel |  | - |  |
-| builder | WidgetBuilder | - | 控件构建器 |
-| close | VoidCallback? | - | 关闭前事件 |
-| focusMove | bool | false | 是否有输入框获取焦点时整体平移避免输入框被遮挡 |
-| isDismissible | bool | true | 点击蒙层能否关闭 |
-| modalBarrierColor | Color? | Colors.black54 | 蒙层颜色 |
-| modalBarrierFull | bool | false | 是否全屏显示蒙层 |
-| modalHeight | double? | - | 弹出框高度 |
-| modalLeft | double? | 0 | 弹出框左侧距离 |
-| modalTop | double? | 0 | 弹出框顶部距离 |
-| modalWidth | double? | - | 弹出框宽度 |
-| open | VoidCallback? | - | 打开前事件 |
-| opened | VoidCallback? | - | 打开后事件 |
-| slideTransitionFrom | SlideTransitionFrom | SlideTransitionFrom.bottom | 设置从屏幕的哪个方向滑出 |
+| context | BuildContext | - | - |
+| options | TPopupOptions | - | 创建时传入的配置;每次 [open] 会按 [TPopupOptions.placement] 裁剪无效字段后使用。 |
+| navigatorContext | BuildContext? | - | 与 [TPopup.show] 的 [navigatorContext] 相同。 |
+| useRootNavigator | bool | false | 与 [TPopup.show] 的 [useRootNavigator] 相同。 |
 
-```
-```
 
-### TPopupBottomDisplayPanel
+### TPopupOptions
 #### 简介
-右上角带关闭的底部浮层面板
+[TPopup.show] 的配置对象。
+
+ ## 如何创建
+
+ | 场景 | 推荐用法 |
+ |------|----------|
+ | 弹出方向已知 | [TPopupOptions.bottom]、[TPopupOptions.center]、[TPopupOptions.top]、[TPopupOptions.left]、[TPopupOptions.right] |
+ | 方向由变量决定 | 默认构造并设置 [placement];传错字段会在 [TPopup.show] / [TPopupHandle.open] 时抛 [FlutterError] |
+
+ 命名工厂只暴露当前方向生效的字段(例如 [TPopupOptions.bottom] 无 [width] 参数)。
+
+ ## 字段与 [TPopupPlacement]
+
+ | [TPopupPlacement] | 头部 / 关闭区 | 尺寸 |
+ |-------------------|-------------|------|
+ | [TPopupPlacement.bottom] | [headerBuilder]、[titleWidget]、[cancelBuilder]、[confirmBuilder] | [height]、[inset] |
+ | [TPopupPlacement.center] | [closeBuilder] | [width]、[height] |
+ | [TPopupPlacement.top] | — | [height]、[inset] |
+ | [TPopupPlacement.left]、[TPopupPlacement.right] | — | [width]、[inset] |
+
+ ## Builder 三态([headerBuilder]、[cancelBuilder]、[confirmBuilder]、[closeBuilder])
+
+ | 传参方式 | 效果 |
+ |----------|------|
+ | 省略(使用默认值) | 渲染内置 UI |
+ | 显式 `null` | 隐藏该区域 |
+ | 自定义 [TPopupHeaderBuilder] / [TPopupSlotBuilder] | 完全替换;需自行提供交互与语义,可调用 `close` 关闭浮层 |
+
+ [titleWidget] 默认为 `null`,表示无标题内容。
+
+ 生命周期回调见 [onOpen]、[onOpened]、[onClose]、[onClosed]、[onVisibleChange]、[onOverlayClick]。
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| backgroundColor |  | - |  |
-| child |  | - |  |
-| closeClick | PopupClick? | - | 关闭按钮点击回调 |
-| closeColor | Color? | - | 关闭按钮颜色 |
-| closeSize | double? | - | 关闭按钮图标尺寸 |
-| draggable |  | - |  |
-| fixedHeight |  | - |  |
-| hideClose | bool | false | 是否隐藏关闭按钮 |
-| key |  | - |  |
-| maxHeightRatio |  | - |  |
-| minHeightRatio |  | - |  |
-| radius |  | - |  |
-| title |  | - |  |
-| titleColor |  | - |  |
-| titleFontSize | double? | - | 标题字体大小 |
-| titleLeft | bool | false | 标题是否靠左 |
+| animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 |
+| backgroundColor | Color? | - | 内容区背景色,默认主题容器色。 |
+| cancelBuilder | TPopupSlotBuilder? | _kPopupDefaultCancel | bottom 左侧操作槽;仅 [headerBuilder] 为内置默认时生效。 内置默认为「取消」,点击触发 [TPopupTrigger.cancel]。 |
+| child | Widget | - | 浮层主体内容(必填)。 |
+| closeBuilder | TPopupSlotBuilder? | _kPopupDefaultClose | center 面板外下方关闭区;仅 [TPopupPlacement.center] 生效。三态见类文档「Builder 三态」。 内置默认点击触发 [TPopupTrigger.close]。 |
+| closeOnOverlayClick | bool? | - | - |
+| confirmBuilder | TPopupSlotBuilder? | _kPopupDefaultConfirm | bottom 右侧操作槽;仅 [headerBuilder] 为内置默认时生效。 内置默认为「确定」,点击触发 [TPopupTrigger.confirm]。 |
+| destroyOnClose | bool | false | 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 |
+| headerBuilder | TPopupHeaderBuilder? | _kPopupDefaultHeader | bottom 头部;仅 [TPopupPlacement.bottom] 生效。三态见类文档「Builder 三态」。 自定义时忽略 [titleWidget]、[cancelBuilder]、[confirmBuilder]。 |
+| height | double? | - | 高度;[TPopupPlacement.top]、[TPopupPlacement.bottom] 生效;[TPopupPlacement.center] 约束面板尺寸。 |
+| inset | TPopupInset? | - | 交叉轴边缘留白;具体类型由 [placement] 决定。 * [TPopupPlacement.bottom] 使用 [TPopupBottomInset] * [TPopupPlacement.top] 使用 [TPopupTopInset] * [TPopupPlacement.left] 使用 [TPopupLeftInset] * [TPopupPlacement.right] 使用 [TPopupRightInset] * [TPopupPlacement.center] 不支持 |
+| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 [showOverlay] 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
+| onClose | VoidCallback? | - | 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 |
+| onClosed | VoidCallback? | - | 当前展示周期真正结束。 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 |
+| onOpen | VoidCallback? | - | 路由 push 时(打开动画开始前)。 |
+| onOpened | VoidCallback? | - | 打开动画结束。 |
+| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 |
+| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 [TPopupTrigger]。 |
+| overlayColor | Color? | - | 蒙层颜色,默认 black54。 |
+| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 |
+| placement | TPopupPlacement | TPopupPlacement.bottom | 出现位置,默认 [TPopupPlacement.bottom]。 |
+| radius | double? | - | 内容区圆角,默认主题大圆角。 |
+| showOverlay | bool | true | 是否绘制半透明蒙层。 当 [modal] 为 true 且此值为 false 时,为“透明模态弹层”。 |
+| titleWidget | Widget? | - | bottom 标题插槽;仅 [headerBuilder] 为内置默认时生效。`null` 表示无标题。 |
+| width | double? | - | 宽度;[TPopupPlacement.left]、[TPopupPlacement.right]、[TPopupPlacement.center] 生效。 |
+
+
+#### 工厂构造方法
+
+##### TPopupOptions.bottom
+
+创建 [TPopupPlacement.bottom] 配置。
+
+ 固定 [placement] 为 [TPopupPlacement.bottom];默认带内置头部。
+ 蒙层、动画、生命周期等字段语义见同名成员文档。
 
-```
-```
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| child | Widget | - | 浮层主体内容(必填)。 |
+| height | double? | - | 高度;[TPopupPlacement.top]、[TPopupPlacement.bottom] 生效;[TPopupPlacement.center] 约束面板尺寸。 |
+| inset | TPopupBottomInset? | - | 交叉轴边缘留白;具体类型由 [placement] 决定。 * [TPopupPlacement.bottom] 使用 [TPopupBottomInset] * [TPopupPlacement.top] 使用 [TPopupTopInset] * [TPopupPlacement.left] 使用 [TPopupLeftInset] * [TPopupPlacement.right] 使用 [TPopupRightInset] * [TPopupPlacement.center] 不支持 |
+| headerBuilder | TPopupHeaderBuilder? | _kPopupDefaultHeader | bottom 头部;仅 [TPopupPlacement.bottom] 生效。三态见类文档「Builder 三态」。 自定义时忽略 [titleWidget]、[cancelBuilder]、[confirmBuilder]。 |
+| titleWidget | Widget? | - | bottom 标题插槽;仅 [headerBuilder] 为内置默认时生效。`null` 表示无标题。 |
+| cancelBuilder | TPopupSlotBuilder? | _kPopupDefaultCancel | bottom 左侧操作槽;仅 [headerBuilder] 为内置默认时生效。 内置默认为「取消」,点击触发 [TPopupTrigger.cancel]。 |
+| confirmBuilder | TPopupSlotBuilder? | _kPopupDefaultConfirm | bottom 右侧操作槽;仅 [headerBuilder] 为内置默认时生效。 内置默认为「确定」,点击触发 [TPopupTrigger.confirm]。 |
+| radius | double? | - | 内容区圆角,默认主题大圆角。 |
+| backgroundColor | Color? | - | 内容区背景色,默认主题容器色。 |
+| showOverlay | bool | true | 是否绘制半透明蒙层。 当 [modal] 为 true 且此值为 false 时,为“透明模态弹层”。 |
+| closeOnOverlayClick | bool? | - | - |
+| overlayColor | Color? | - | 蒙层颜色,默认 black54。 |
+| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 |
+| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 [showOverlay] 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
+| destroyOnClose | bool | false | 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 |
+| animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 |
+| onOpen | VoidCallback? | - | 路由 push 时(打开动画开始前)。 |
+| onOpened | VoidCallback? | - | 打开动画结束。 |
+| onClose | VoidCallback? | - | 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 |
+| onClosed | VoidCallback? | - | 当前展示周期真正结束。 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 |
+| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 [TPopupTrigger]。 |
+| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 |
+
+
+##### TPopupOptions.center
+
+创建 [TPopupPlacement.center] 配置。
+
+ 固定 [placement] 为 [TPopupPlacement.center];默认展示面板外下方圆形关闭按钮。
 
-### TPopupBottomConfirmPanel
-#### 简介
-带确认的底部浮层面板
-#### 默认构造方法
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| child | Widget | - | 浮层主体内容(必填)。 |
+| width | double? | - | 宽度;[TPopupPlacement.left]、[TPopupPlacement.right]、[TPopupPlacement.center] 生效。 |
+| height | double? | - | 高度;[TPopupPlacement.top]、[TPopupPlacement.bottom] 生效;[TPopupPlacement.center] 约束面板尺寸。 |
+| closeBuilder | TPopupSlotBuilder? | _kPopupDefaultClose | center 面板外下方关闭区;仅 [TPopupPlacement.center] 生效。三态见类文档「Builder 三态」。 内置默认点击触发 [TPopupTrigger.close]。 |
+| radius | double? | - | 内容区圆角,默认主题大圆角。 |
+| backgroundColor | Color? | - | 内容区背景色,默认主题容器色。 |
+| showOverlay | bool | true | 是否绘制半透明蒙层。 当 [modal] 为 true 且此值为 false 时,为“透明模态弹层”。 |
+| closeOnOverlayClick | bool? | - | - |
+| overlayColor | Color? | - | 蒙层颜色,默认 black54。 |
+| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 |
+| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 [showOverlay] 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
+| destroyOnClose | bool | false | 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 |
+| animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 |
+| onOpen | VoidCallback? | - | 路由 push 时(打开动画开始前)。 |
+| onOpened | VoidCallback? | - | 打开动画结束。 |
+| onClose | VoidCallback? | - | 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 |
+| onClosed | VoidCallback? | - | 当前展示周期真正结束。 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 |
+| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 [TPopupTrigger]。 |
+| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 |
+
+
+##### TPopupOptions.left
+
+创建 [TPopupPlacement.left] 配置。
+
+ 固定 [placement] 为 [TPopupPlacement.left];未传 [width] 时布局默认宽度 280。
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| backgroundColor |  | - |  |
-| child |  | - |  |
-| draggable |  | - |  |
-| key |  | - |  |
-| leftClick | PopupClick? | - | 左边文本点击回调 |
-| leftText | String? | - | 左边文本 |
-| leftTextColor | Color? | - | 左边文本颜色 |
-| leftTextFontSize | double? | - | 左边文本字体大小 |
-| maxHeightRatio |  | - |  |
-| minHeightRatio |  | - |  |
-| radius |  | - |  |
-| rightClick | PopupClick? | - | 右边文本点击回调 |
-| rightText | String? | - | 右边文本 |
-| rightTextColor | Color? | - | 右边文本颜色 |
-| rightTextFontSize | double? | - | 右边文本字体大小 |
-| title |  | - |  |
-| titleColor |  | - |  |
-| titleFontSize | double? | - | 标题字体大小 |
+| child | Widget | - | 浮层主体内容(必填)。 |
+| width | double? | - | 宽度;[TPopupPlacement.left]、[TPopupPlacement.right]、[TPopupPlacement.center] 生效。 |
+| inset | TPopupLeftInset? | - | 交叉轴边缘留白;具体类型由 [placement] 决定。 * [TPopupPlacement.bottom] 使用 [TPopupBottomInset] * [TPopupPlacement.top] 使用 [TPopupTopInset] * [TPopupPlacement.left] 使用 [TPopupLeftInset] * [TPopupPlacement.right] 使用 [TPopupRightInset] * [TPopupPlacement.center] 不支持 |
+| radius | double? | - | 内容区圆角,默认主题大圆角。 |
+| backgroundColor | Color? | - | 内容区背景色,默认主题容器色。 |
+| showOverlay | bool | true | 是否绘制半透明蒙层。 当 [modal] 为 true 且此值为 false 时,为“透明模态弹层”。 |
+| closeOnOverlayClick | bool? | - | - |
+| overlayColor | Color? | - | 蒙层颜色,默认 black54。 |
+| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 |
+| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 [showOverlay] 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
+| destroyOnClose | bool | false | 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 |
+| animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 |
+| onOpen | VoidCallback? | - | 路由 push 时(打开动画开始前)。 |
+| onOpened | VoidCallback? | - | 打开动画结束。 |
+| onClose | VoidCallback? | - | 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 |
+| onClosed | VoidCallback? | - | 当前展示周期真正结束。 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 |
+| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 [TPopupTrigger]。 |
+| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 |
+
+
+##### TPopupOptions.right
+
+创建 [TPopupPlacement.right] 配置。
+
+ 固定 [placement] 为 [TPopupPlacement.right];未传 [width] 时布局默认宽度 280。
 
-```
-```
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| child | Widget | - | 浮层主体内容(必填)。 |
+| width | double? | - | 宽度;[TPopupPlacement.left]、[TPopupPlacement.right]、[TPopupPlacement.center] 生效。 |
+| inset | TPopupRightInset? | - | 交叉轴边缘留白;具体类型由 [placement] 决定。 * [TPopupPlacement.bottom] 使用 [TPopupBottomInset] * [TPopupPlacement.top] 使用 [TPopupTopInset] * [TPopupPlacement.left] 使用 [TPopupLeftInset] * [TPopupPlacement.right] 使用 [TPopupRightInset] * [TPopupPlacement.center] 不支持 |
+| radius | double? | - | 内容区圆角,默认主题大圆角。 |
+| backgroundColor | Color? | - | 内容区背景色,默认主题容器色。 |
+| showOverlay | bool | true | 是否绘制半透明蒙层。 当 [modal] 为 true 且此值为 false 时,为“透明模态弹层”。 |
+| closeOnOverlayClick | bool? | - | - |
+| overlayColor | Color? | - | 蒙层颜色,默认 black54。 |
+| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 |
+| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 [showOverlay] 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
+| destroyOnClose | bool | false | 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 |
+| animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 |
+| onOpen | VoidCallback? | - | 路由 push 时(打开动画开始前)。 |
+| onOpened | VoidCallback? | - | 打开动画结束。 |
+| onClose | VoidCallback? | - | 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 |
+| onClosed | VoidCallback? | - | 当前展示周期真正结束。 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 |
+| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 [TPopupTrigger]。 |
+| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 |
+
+
+##### TPopupOptions.top
+
+创建 [TPopupPlacement.top] 配置。
+
+ 固定 [placement] 为 [TPopupPlacement.top];无内置头部。
 
-### TPopupCenterPanel
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| child | Widget | - | 浮层主体内容(必填)。 |
+| height | double? | - | 高度;[TPopupPlacement.top]、[TPopupPlacement.bottom] 生效;[TPopupPlacement.center] 约束面板尺寸。 |
+| inset | TPopupTopInset? | - | 交叉轴边缘留白;具体类型由 [placement] 决定。 * [TPopupPlacement.bottom] 使用 [TPopupBottomInset] * [TPopupPlacement.top] 使用 [TPopupTopInset] * [TPopupPlacement.left] 使用 [TPopupLeftInset] * [TPopupPlacement.right] 使用 [TPopupRightInset] * [TPopupPlacement.center] 不支持 |
+| radius | double? | - | 内容区圆角,默认主题大圆角。 |
+| backgroundColor | Color? | - | 内容区背景色,默认主题容器色。 |
+| showOverlay | bool | true | 是否绘制半透明蒙层。 当 [modal] 为 true 且此值为 false 时,为“透明模态弹层”。 |
+| closeOnOverlayClick | bool? | - | - |
+| overlayColor | Color? | - | 蒙层颜色,默认 black54。 |
+| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 |
+| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 [showOverlay] 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
+| destroyOnClose | bool | false | 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 |
+| animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 |
+| onOpen | VoidCallback? | - | 路由 push 时(打开动画开始前)。 |
+| onOpened | VoidCallback? | - | 打开动画结束。 |
+| onClose | VoidCallback? | - | 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 |
+| onClosed | VoidCallback? | - | 当前展示周期真正结束。 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 |
+| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 [TPopupTrigger]。 |
+| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 |
+
+
+### TPopupHandle
 #### 简介
-居中浮层面板
-#### 默认构造方法
+[TPopup.show] 的返回值,用于控制同一份 [TPopupOptions] 的多次打开与关闭。
+
+ **示例**
+
+ ```dart
+ final handle = TPopup.show(
+   context,
+   options: TPopupOptions.bottom(child: panel),
+ );
+ handle.close();
+ handle.open(); // 可省略 context,复用已缓存的 Navigator
+ ```
+#### 公开属性
+
+| 属性 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| navigatorContext | BuildContext? | - | 与 [TPopup.show] 的 [navigatorContext] 相同。 |
+| options | TPopupOptions | - | 创建时传入的配置;每次 [open] 会按 [TPopupOptions.placement] 裁剪无效字段后使用。 |
+| useRootNavigator | bool | - | 与 [TPopup.show] 的 [useRootNavigator] 相同。 |
+
+
+#### 工厂构造方法
+
+##### TPopupHandle._
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| backgroundColor | Color? | - | 背景颜色 |
-| child | Widget | - | 子控件 |
-| closeClick | PopupClick? | - | 关闭按钮点击回调 |
-| closeColor | Color? | - | 关闭按钮颜色 |
-| closeSize | double? | - | 关闭按钮图标尺寸 |
-| closeUnderBottom | bool | false | 关闭按钮是否在视图框下方 |
-| key |  | - |  |
-| radius | double? | - | 圆角 |
+| options | TPopupOptions | - | 创建时传入的配置;每次 [open] 会按 [TPopupOptions.placement] 裁剪无效字段后使用。 |
+| navigatorContext | BuildContext? | - | 与 [TPopup.show] 的 [navigatorContext] 相同。 |
+| useRootNavigator | bool | false | 与 [TPopup.show] 的 [useRootNavigator] 相同。 |
+
+
+### TPopupPlacement
+#### 简介
+浮层出现方向;决定 [TPopupOptions] 中哪些字段生效。
+
+ 与 [TPopupOptions] 类文档中的「字段与 placement」表对应。
+ 方向固定时请用 [TPopupOptions.bottom]、[TPopupOptions.center] 等命名工厂。
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| top | 自顶部滑入;使用 [TPopupOptions.height]、[TPopupOptions.inset]([TPopupTopInset])。 |
+| left | 自左侧滑入;使用 [TPopupOptions.width]、[TPopupOptions.inset]([TPopupLeftInset])。 |
+| right | 自右侧滑入;使用 [TPopupOptions.width]、[TPopupOptions.inset]([TPopupRightInset])。 |
+| bottom | 自底部滑入;默认内置头部;使用 [TPopupOptions.height]、[TPopupOptions.inset]([TPopupBottomInset])。 |
+| center | 屏幕居中;使用 [TPopupOptions.closeBuilder] 控制面板外下方关闭区。 |
+
+
+### TPopupTrigger
+#### 简介
+浮层关闭或显隐变化时的触发来源。
+
+ 作为 [TPopupVisibleChangeCallback] 的第二个参数,以及关闭流程中的语义标记。
+
+ 内置控件会映射为 [TPopupTrigger.overlay]、[TPopupTrigger.cancel]、
+ [TPopupTrigger.confirm]、[TPopupTrigger.close];
+ [TPopupHandle.close] 为 [TPopupTrigger.api];系统返回为
+ [TPopupTrigger.systemBack];[headerBuilder] 内调用 `close` 等为
+ [TPopupTrigger.custom]。
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| overlay | 点击蒙层,且 [TPopupOptions.closeOnOverlayClick] 为 true。 |
+| cancel | 点击 bottom 取消语义槽位(含默认与自定义 [TPopupOptions.cancelBuilder])。 |
+| confirm | 点击 bottom 确认语义槽位(含默认与自定义 [TPopupOptions.confirmBuilder])。 |
+| close | 点击 center 关闭语义槽位(含默认与自定义 [TPopupOptions.closeBuilder])。 |
+| api | 外部 API 主动触发的显隐变化,如 [TPopupHandle.close] 或打开事件。 |
+| systemBack | 系统返回键或系统路由返回触发的关闭。 |
+| custom | 无框架预设动作语义的自定义关闭,如 [headerBuilder] 内调用 `close`。 |
 
 
   
\ No newline at end of file
diff --git a/tdesign-site/src/progress/README.md b/tdesign-site/src/progress/README.md
index 67c7a2a62..5c04f47bb 100644
--- a/tdesign-site/src/progress/README.md
+++ b/tdesign-site/src/progress/README.md
@@ -323,12 +323,12 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| animationDuration | int? | 300 | 动画持续时间(正整数,单位为毫秒) |
+| animationDuration | int | 300 | 动画持续时间(正整数,单位为毫秒) |
 | backgroundColor | Color? | - | 进度条背景颜色 |
 | circleRadius | double? | - | 环形进度条半径(正数) |
 | color | Color? | - | 进度条颜色 |
 | customProgressLabel | Widget? | - | 自定义标签 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | label | TLabelWidget? | - | 进度条标签 |
 | labelWidgetAlignment | Alignment? | - | 自定义标签对齐方式 |
 | labelWidgetWidth | double? | - | 自定义标签宽度 |
@@ -343,4 +343,39 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | value | double? | - | 进度值(0.0 到 1.0 之间的正数) |
 
 
+### TProgressType
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| linear | - |
+| circular | - |
+| micro | - |
+| button | - |
+
+
+### TProgressLabelPosition
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| inside | - |
+| left | - |
+| right | - |
+
+
+### TProgressStatus
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| primary | - |
+| warning | - |
+| danger | - |
+| success | - |
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/pull-down-refresh/README.md b/tdesign-site/src/pull-down-refresh/README.md
index d063b28cf..8bc3e0705 100644
--- a/tdesign-site/src/pull-down-refresh/README.md
+++ b/tdesign-site/src/pull-down-refresh/README.md
@@ -90,41 +90,41 @@ TDesign刷新头部
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
 | backgroundColor | Color? | - | 背景颜色 |
-| clamping |  | - |  |
+| clamping | bool? | - | - |
 | completeDuration | Duration? | - | 完成延时 |
 | enableHapticFeedback | bool | true | 开启震动反馈 |
 | enableInfiniteRefresh | bool | false | 是否开启无限刷新 |
 | extent | double? | 48.0 | Header容器高度 |
 | float | bool | false | 是否悬浮 |
-| frictionFactor |  | - |  |
-| hapticFeedback |  | - |  |
-| hitOver |  | - |  |
-| horizontalFrictionFactor |  | - |  |
-| horizontalReadySpringBuilder |  | - |  |
-| horizontalSpring |  | - |  |
-| infiniteHitOver |  | - |  |
+| frictionFactor | - | - | - |
+| hapticFeedback | bool? | - | - |
+| hitOver | - | - | - |
+| horizontalFrictionFactor | - | - | - |
+| horizontalReadySpringBuilder | - | - | - |
+| horizontalSpring | - | - | - |
+| infiniteHitOver | bool? | - | - |
 | infiniteOffset | double? | - | 无限刷新偏移量 |
 | key | Key? | - | Key |
-| listenable |  | - |  |
+| listenable | - | - | - |
 | loadingIcon | TLoadingIcon | TLoadingIcon.circle | loading样式 |
-| maxOverOffset |  | - |  |
-| notifyWhenInvisible |  | - |  |
+| maxOverOffset | - | - | - |
+| notifyWhenInvisible | - | - | - |
 | overScroll | bool | true | 越界滚动([enableInfiniteRefresh]为true或[infiniteOffset]有值时生效) |
-| position |  | - |  |
-| processedDuration |  | - |  |
-| readySpringBuilder |  | - |  |
-| safeArea |  | false |  |
-| secondaryCloseTriggerOffset |  | - |  |
-| secondaryDimension |  | - |  |
-| secondaryTriggerOffset |  | - |  |
-| secondaryVelocity |  | - |  |
-| spring |  | - |  |
-| springRebound |  | - |  |
+| position | - | - | - |
+| processedDuration | Duration? | - | - |
+| readySpringBuilder | - | - | - |
+| safeArea | - | false | - |
+| secondaryCloseTriggerOffset | - | - | - |
+| secondaryDimension | - | - | - |
+| secondaryTriggerOffset | - | - | - |
+| secondaryVelocity | - | - | - |
+| spring | - | - | - |
+| springRebound | - | - | - |
 | triggerDistance | double | 48.0 | 触发刷新任务的偏移量,同[triggerOffset] |
-| triggerOffset |  | - |  |
-| triggerWhenReach |  | - |  |
-| triggerWhenRelease |  | - |  |
-| triggerWhenReleaseNoWait |  | - |  |
+| triggerOffset | double? | - | - |
+| triggerWhenReach | - | - | - |
+| triggerWhenRelease | - | - | - |
+| triggerWhenReleaseNoWait | - | - | - |
 
 
   
\ No newline at end of file
diff --git a/tdesign-site/src/radio/README.md b/tdesign-site/src/radio/README.md
index 7464c515b..4c9136787 100644
--- a/tdesign-site/src/radio/README.md
+++ b/tdesign-site/src/radio/README.md
@@ -338,34 +338,32 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| backgroundColor |  | - |  |
-| cardMode |  | - |  |
-| checkBoxLeftSpace |  | - |  |
-| contentDirection |  | TContentDirection.right |  |
-| customContentBuilder |  | - |  |
-| customIconBuilder |  | - |  |
-| customSpace |  | - |  |
-| disableColor |  | - |  |
-| enable |  | true |  |
-| id |  | - |  |
-| insetSpacing |  | - |  |
-| key |  | - |  |
+| backgroundColor | Color? | - | - |
+| cardMode | bool? | - | - |
+| checkBoxLeftSpace | double? | - | - |
+| contentDirection | TContentDirection | TContentDirection.right | - |
+| customContentBuilder | ContentBuilder? | - | - |
+| customIconBuilder | IconBuilder? | - | - |
+| customSpace | EdgeInsetsGeometry? | - | - |
+| disableColor | Color? | - | - |
+| enable | bool | true | - |
+| id | String? | - | - |
+| insetSpacing | double? | - | - |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | radioStyle | TRadioStyle | TRadioStyle.circle | 单选框按钮样式 |
-| selectColor |  | - |  |
-| showDivider | bool | - | 是否显示下划线 |
-| size |  | TCheckBoxSize.small |  |
-| spacing |  | - |  |
-| subTitle |  | - |  |
-| subTitleColor |  | - |  |
-| subTitleFont |  | - |  |
-| subTitleMaxLine |  | 1 |  |
-| title |  | - |  |
-| titleColor |  | - |  |
-| titleFont |  | - |  |
-| titleMaxLine |  | 1 |  |
+| selectColor | Color? | - | - |
+| showDivider | bool? | - | - |
+| size | TCheckBoxSize | TCheckBoxSize.small | - |
+| spacing | double? | - | - |
+| subTitle | String? | - | - |
+| subTitleColor | Color? | - | - |
+| subTitleFont | Font? | - | - |
+| subTitleMaxLine | int | 1 | - |
+| title | String? | - | - |
+| titleColor | Color? | - | - |
+| titleFont | Font? | - | - |
+| titleMaxLine | int | 1 | - |
 
-```
-```
 
 ### TRadioGroup
 #### 简介
@@ -378,25 +376,45 @@ RadioGroup分组对象,继承自TCheckboxGroup,字段含义与父类一致
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| cardMode |  | false |  |
-| child |  | - |  |
-| contentDirection |  | - |  |
-| controller |  | - |  |
-| customContentBuilder |  | - |  |
-| customIconBuilder |  | - |  |
-| direction |  | - |  |
-| directionalTdRadios |  | - |  |
+| cardMode | bool | false | - |
+| child | Widget? | - | - |
+| contentDirection | TContentDirection? | - | - |
+| controller | TCheckboxGroupController? | - | - |
+| customContentBuilder | ContentBuilder? | - | - |
+| customIconBuilder | IconBuilder? | - | - |
+| direction | Axis? | - | - |
+| directionalTdRadios | List? | - | - |
 | divider | Widget? | - | 自定义下划线 |
-| key |  | - |  |
-| onRadioGroupChange |  | - |  |
-| passThrough |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
+| onRadioGroupChange | OnRadioGroupChange? | - | - |
+| passThrough | bool? | - | - |
 | radioCheckStyle | TRadioStyle? | - | 勾选样式 |
 | rowCount | int | 1 | 每行几列 |
-| selectId |  | - |  |
+| selectId | String? | - | - |
 | showDivider | bool | false | 是否显示下划线 |
-| spacing |  | - |  |
+| spacing | double? | - | - |
 | strictMode | bool | true | 严格模式下,用户不能取消勾选,只能切换选择项, |
-| titleMaxLine |  | - |  |
+| titleMaxLine | int? | - | - |
+
+
+### TRadioStyle
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| circle | - |
+| square | - |
+| check | - |
+| hollowCircle | - |
+
+
+### OnRadioGroupChange
+#### 类型定义
+
+```dart
+typedef OnRadioGroupChange = void Function(String? selectedId);
+```
 
 
   
\ No newline at end of file
diff --git a/tdesign-site/src/rate/README.md b/tdesign-site/src/rate/README.md
index f5472215c..5d53b6ba7 100644
--- a/tdesign-site/src/rate/README.md
+++ b/tdesign-site/src/rate/README.md
@@ -217,7 +217,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
 | allowHalf | bool? | false | 是否允许半选 |
-| builderText | Widget Function(BuildContext context, double value)? | - | 评分等级对应的辅助文字自定义构建,优先级高于[texts] |
+| builderText | Widget Function(BuildContext context, double value)? | - | 评分等级对应的辅助文字自定义构建,优先级高于[texts] 配置后,会忽略[texts],[textWidth],[iconTextGap] |
 | color | List? | - | 评分图标的颜色,示例:[选中颜色] / [选中颜色,未选中颜色],默认:[TTheme.of(context).warningColor5, TTheme.of(context).grayColor4] |
 | count | int? | 5 | 评分的数量 |
 | crossAxisAlignment | CrossAxisAlignment? | CrossAxisAlignment.center | 评分图标与辅助文字的交叉轴对齐方式 |
@@ -226,16 +226,27 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | gap | double? | - | 评分图标的间距,默认:TTheme.of(context).spacer8 |
 | icon | List? | - | 自定义评分图标,[选中和未选中图标] / [选中图标,未选中图标],默认:[TIcons.star_filled] |
 | iconTextGap | double? | - | 评分图标与辅助文字的间距,默认:[TTheme.of(context).spacer16] |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | mainAxisAlignment | MainAxisAlignment? | MainAxisAlignment.start | 评分图标与辅助文字的主轴对齐方式 |
 | mainAxisSize | MainAxisSize? | MainAxisSize.min | 评分图标与辅助文字主轴方向上如何占用空间 |
 | onChange | void Function(double value)? | - | 评分数改变时触发 |
 | placement | PlacementEnum? | PlacementEnum.top | 选择评分弹框的位置,值为[PlacementEnum.none]表示不显示评分弹框。 |
 | showText | bool? | false | 是否显示对应的辅助文字 |
 | size | double? | 24.0 | 评分图标的大小 |
-| texts | List? | const ['极差', '失望', '一般', '满意', '惊喜'] | 评分等级对应的辅助文字, |
+| texts | List? | const ['极差', '失望', '一般', '满意', '惊喜'] | 评分等级对应的辅助文字, 当[allowHalf]为false时长度应与[count]一致, 当[allowHalf]为true时长度应为[count]的两倍, 自定义值示例:['1分', '2分', '3分', '4分', '5分']。 |
 | textWidth | double? | 48.0 | 评分等级对应的辅助文字宽度 |
 | value | double? | 0 | 选择评分的值 |
 
 
+### PlacementEnum
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| none | - |
+| top | - |
+| bottom | - |
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/result/README.md b/tdesign-site/src/result/README.md
index 47fa4faf5..9763982e9 100644
--- a/tdesign-site/src/result/README.md
+++ b/tdesign-site/src/result/README.md
@@ -290,10 +290,22 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | --- | --- | --- | --- |
 | description | String? | - | 描述文本,用于提供额外信息 |
 | icon | Widget? | - | 图标组件,用于在结果中显示一个图标 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | theme | TResultTheme | TResultTheme.defaultTheme | 主题样式,默认主题样式为defaultTheme |
 | title | String | '' | 标题文本,显示结果的主要信息,默认标题为空字符串 |
 | titleStyle | TextStyle? | - | 自定义字体样式,用于设置标题文本的样式 |
 
 
+### TResultTheme
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| defaultTheme | - |
+| success | - |
+| warning | - |
+| error | - |
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/search/README.md b/tdesign-site/src/search/README.md
index 51146c4ee..93b973a62 100644
--- a/tdesign-site/src/search/README.md
+++ b/tdesign-site/src/search/README.md
@@ -130,7 +130,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | enabled | bool? | - | 是否禁用 |
 | focusNode | FocusNode? | - | 自定义焦点 |
 | inputAction | TextInputAction? | - | 键盘动作类型 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | mediumStyle | bool | false | 是否在导航栏中的样式 |
 | needCancel | bool | false | 是否需要取消按钮 |
 | onActionClick | TSearchBarEvent? | - | 自定义操作回调 |
@@ -146,4 +146,52 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | style | TSearchStyle? | TSearchStyle.square | 样式 |
 
 
+### TSearchStyle
+#### 简介
+搜索框的样式
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| square | 方形 |
+| round | 圆形 |
+
+
+### TSearchAlignment
+#### 简介
+搜索框对齐方式
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| left | 默认头部对齐 |
+| center | 居中 |
+
+
+### TSearchBarEvent
+#### 类型定义
+
+```dart
+typedef TSearchBarEvent = void Function(String value);
+```
+
+
+### TSearchBarClearEvent
+#### 类型定义
+
+```dart
+typedef TSearchBarClearEvent = bool? Function(String value);
+```
+
+
+### TSearchBarCallBack
+#### 类型定义
+
+```dart
+typedef TSearchBarCallBack = void Function();
+```
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/side-bar/README.md b/tdesign-site/src/side-bar/README.md
index 9e5e9cc99..b68ec1183 100644
--- a/tdesign-site/src/side-bar/README.md
+++ b/tdesign-site/src/side-bar/README.md
@@ -645,7 +645,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | controller | TSideBarController? | - | 控制器 |
 | defaultValue | int? | - | 默认值 |
 | height | double? | - | 高度 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | loading | bool? | - | 加载效果 |
 | loadingWidget | Widget? | - | 自定义加载动画 |
 | onChanged | ValueChanged? | - | 选中值发生变化(Controller控制) |
@@ -658,8 +658,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | unSelectedColor | Color? | - | 未选中颜色 |
 | value | int? | - | 选项值 |
 
-```
-```
 
 ### TSideBarItem
 #### 默认构造方法
@@ -669,10 +667,20 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | badge | TBadge? | - | 徽标 |
 | disabled | bool | false | 是否禁用 |
 | icon | IconData? | - | 图标 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | label | String | '' | 标签 |
 | textStyle | TextStyle? | - | 标签样式 |
 | value | int | -1 | 值 |
 
 
+### TSideBarStyle
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| normal | - |
+| outline | - |
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/slider/README.md b/tdesign-site/src/slider/README.md
index 781aa3218..6e309ab97 100644
--- a/tdesign-site/src/slider/README.md
+++ b/tdesign-site/src/slider/README.md
@@ -772,19 +772,17 @@ onThumbTextTap
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
 | boxDecoration | Decoration? | - | 自定义盒子样式 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | leftLabel | String? | - | 左侧标签 |
-| onChanged | ValueChanged? | - | 滑动变化监听 |
-| onChangeEnd | ValueChanged? | - | 滑动结束监听 |
-| onChangeStart | ValueChanged? | - | 滑动开始监听 |
-| onTap |  Function(Position position, Offset offset, double value)? | - | Thumb 点击事件 位置、坐标、当前值 |
-| onThumbTextTap |  Function(Position position, Offset offset, double value)? | - | Thumb 点击浮标文字 位置、坐标、当前值 |
+| onChanged | ValueChanged? | - | 滑动变化监听 |
+| onChangeEnd | ValueChanged? | - | 滑动结束监听 |
+| onChangeStart | ValueChanged? | - | 滑动开始监听 |
+| onTap | Function(Offset offset, double value)? | - | Thumb 点击事件 坐标、当前值 |
+| onThumbTextTap | Function(Offset offset, double value)? | - | Thumb 点击浮标文字 坐标、当前值 |
 | rightLabel | String? | - | 右侧标签 |
 | sliderThemeData | TSliderThemeData? | - | 样式 |
-| value | RangeValues | - | 默认值 |
+| value | double | - | 默认值 |
 
-```
-```
 
 ### TRangeSlider
 #### 默认构造方法
@@ -792,16 +790,26 @@ onThumbTextTap
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
 | boxDecoration | Decoration? | - | 自定义盒子样式 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | leftLabel | String? | - | 左侧标签 |
 | onChanged | ValueChanged? | - | 滑动变化监听 |
 | onChangeEnd | ValueChanged? | - | 滑动结束监听 |
 | onChangeStart | ValueChanged? | - | 滑动开始监听 |
-| onTap |  Function(Position position, Offset offset, double value)? | - | Thumb 点击事件 位置、坐标、当前值 |
-| onThumbTextTap |  Function(Position position, Offset offset, double value)? | - | Thumb 点击浮标文字 位置、坐标、当前值 |
+| onTap | Function(Position position, Offset offset, double value)? | - | Thumb 点击事件 位置、坐标、当前值 |
+| onThumbTextTap | Function(Position position, Offset offset, double value)? | - | Thumb 点击浮标文字 位置、坐标、当前值 |
 | rightLabel | String? | - | 右侧标签 |
 | sliderThemeData | TSliderThemeData? | - | 样式 |
 | value | RangeValues | - | 默认值 |
 
 
+### Position
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| start | - |
+| end | - |
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/stepper/README.md b/tdesign-site/src/stepper/README.md
index df9abaeb5..6c8d1f950 100644
--- a/tdesign-site/src/stepper/README.md
+++ b/tdesign-site/src/stepper/README.md
@@ -126,7 +126,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | disableInput | bool | false | 禁用输入框 |
 | eventController | StreamController? | - | 事件控制器 |
 | inputWidth | double? | - | 禁用全部操作 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | max | int | 100 | 最大值 |
 | min | int | 0 | 最小值 |
 | onBlur | VoidCallback? | - | 输入框失去焦点时触发 |
@@ -138,4 +138,71 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | value | int? | 0 | 值 |
 
 
+### TStepperSize
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| small | - |
+| medium | - |
+| large | - |
+
+
+### TStepperTheme
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| normal | - |
+| filled | - |
+| outline | - |
+
+
+### TStepperIconType
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| remove | - |
+| add | - |
+
+
+### TStepperOverlimitType
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| minus | - |
+| plus | - |
+
+
+### TStepperEventType
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| cleanValue | - |
+
+
+### TStepperOverlimitFunction
+#### 类型定义
+
+```dart
+typedef TStepperOverlimitFunction = void Function(TStepperOverlimitType type);
+```
+
+
+### TTapFunction
+#### 类型定义
+
+```dart
+typedef TTapFunction = void Function();
+```
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/steps/README.md b/tdesign-site/src/steps/README.md
index 2209c4d34..e23a2da3f 100644
--- a/tdesign-site/src/steps/README.md
+++ b/tdesign-site/src/steps/README.md
@@ -710,15 +710,13 @@ Vertical Customize Steps 垂直自定义步骤条
 | --- | --- | --- | --- |
 | activeIndex | int | 0 | 步骤条当前激活的索引 |
 | direction | TStepsDirection | TStepsDirection.horizontal | 步骤条方向 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | readOnly | bool | false | 步骤条readOnly模式 |
 | simple | bool | false | 步骤条simple模式 |
 | status | TStepsStatus | TStepsStatus.success | 步骤条状态 |
 | steps | List | - | 步骤条数据 |
 | verticalSelect | bool | false | 步骤条垂直自定义步骤条选择模式 |
 
-```
-```
 
 ### TStepsItemData
 #### 默认构造方法
@@ -733,4 +731,28 @@ Vertical Customize Steps 垂直自定义步骤条
 | title | String? | - | 标题 |
 
 
+### TStepsDirection
+#### 简介
+Steps步骤条方向
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| horizontal | - |
+| vertical | - |
+
+
+### TStepsStatus
+#### 简介
+steps步骤条状态
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| success | - |
+| error | - |
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/swipe-cell/README.md b/tdesign-site/src/swipe-cell/README.md
index 4d30bc4bc..0947a8d2e 100644
--- a/tdesign-site/src/swipe-cell/README.md
+++ b/tdesign-site/src/swipe-cell/README.md
@@ -346,16 +346,16 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | --- | --- | --- | --- |
 | cell | Widget | - | 单元格 [TCell] |
 | closeWhenOpened | bool? | true | 当同一组([groupTag])中的一个[TSwipeCell]打开时,是否关闭组中的所有其他[TSwipeCell] |
-| closeWhenTapped | bool? | true | 当同一组([groupTag])中的一个[TSwipeCell]被点击时,是否应该关闭组中的所有[TSwipeCell] |
+| closeWhenTapped | bool? | true | 当同一组([groupTag])中的一个[TSwipeCell]被点击时,是否应该关闭组中的所有[TSwipeCell] [cell]组件被点击时必须传递点击事件,执行`TSwipeCellInherited.of(context)?.cellClick()` |
 | controller | SlidableController? | - | 自定义控制滑动窗口 |
 | direction | Axis? | Axis.horizontal | 可拖动的方向 |
 | disabled | bool? | false | 是否禁用滑动 |
 | dragStartBehavior | DragStartBehavior? | DragStartBehavior.start | 处理拖动开始行为的方式[GestureDetector.dragStartBehavior] |
 | duration | Duration? | const Duration(milliseconds: 200) | 打开关闭动画时长 |
 | groupTag | Object? | - | 组,配置后,[closeWhenOpened]、[closeWhenTapped]才起作用 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | left | TSwipeCellPanel? | - | 左侧滑动操作项面板 |
-| onChange |  Function(TSwipeDirection direction, bool open)? | - | 滑动展开事件 |
+| onChange | Function(TSwipeDirection direction, bool open)? | - | 滑动展开事件 |
 | opened | List? | const [false, false] | 默认打开,[left, right] |
 | right | TSwipeCellPanel? | - | 右侧滑动操作项面板 |
 | slidableKey | Key? | - | 滑动组件的 Key |
@@ -363,10 +363,39 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 #### 静态方法
 
-| 名称 | 返回类型 | 参数 | 说明 |
+##### TSwipeCell.close
+
+根据[groupTag]关闭[TSwipeCell]
+
+ current:保留当前不关闭
+
+返回类型:`void`
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| tag | Object? | - | - |
+| current | SlidableController? | - | - |
+
+
+##### TSwipeCell.of
+
+获取上下文最近的[controller]
+
+返回类型:`SlidableController?`
+
+| 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| close |  |   required Object? tag,  SlidableController? current, | 根据[groupTag]关闭[TSwipeCell]     current:保留当前不关闭 |
-| of |  |   required BuildContext context, | 获取上下文最近的[controller] |
+| context | BuildContext | - | - |
+
+
+### TSwipeDirection
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| right | - |
+| left | - |
 
 
   
\ No newline at end of file
diff --git a/tdesign-site/src/swiper/README.md b/tdesign-site/src/swiper/README.md
index 87b5dda46..1d0d2ed98 100644
--- a/tdesign-site/src/swiper/README.md
+++ b/tdesign-site/src/swiper/README.md
@@ -274,13 +274,20 @@ TDesign风格的Swiper指示器样式,与flutter_swiper的Swiper结合使用
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| alignment | Alignment? | - | 当 scrollDirection== Axis.horizontal 时,默认Alignment.bottomCenter |
+| alignment | Alignment? | - | 当 scrollDirection== Axis.horizontal 时,默认Alignment.bottomCenter 当 scrollDirection== Axis.vertical 时,默认Alignment.centerRight |
 | builder | SwiperPlugin | TSwiperPagination.dots | 具体样式 |
-| key | Key? | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | margin | EdgeInsetsGeometry | const EdgeInsets.all(10.0) | 指示器和container之间的距离 |
 
-```
-```
+#### 静态成员
+
+| 名称 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| controls | SwiperPlugin | - | 箭头样式 |
+| dots | SwiperPlugin | - | 圆点样式 |
+| dotsBar | SwiperPlugin | - | 圆角矩形 + 圆点样式 默认宽度20,高度6 |
+| fraction | SwiperPlugin | - | 数字样式 |
+
 
 ### TPageTransformer
 #### 简介
@@ -296,10 +303,23 @@ TD默认PageTransformer
 
 #### 工厂构造方法
 
-| 名称  | 说明 |
-| --- |  --- |
-| TPageTransformer.margin  | 普通margin的卡片式 |
-| TPageTransformer.scaleAndFade  | 缩放或透明的卡片式 |
+##### TPageTransformer.margin
+
+普通margin的卡片式
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| margin | double? | 6.0 | 左右间隔 |
+
+
+##### TPageTransformer.scaleAndFade
+
+缩放或透明的卡片式
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| fade | double? | 1 | 淡化比例 |
+| scale | double? | 0.8 | 缩放比例 |
 
 
   
\ No newline at end of file
diff --git a/tdesign-site/src/switch/README.md b/tdesign-site/src/switch/README.md
index fa6258b24..a31fbc1cc 100644
--- a/tdesign-site/src/switch/README.md
+++ b/tdesign-site/src/switch/README.md
@@ -227,7 +227,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | closeText | String? | - | 关闭文案 |
 | enable | bool | true | 是否可点击 |
 | isOn | bool | false | 是否打开 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | onChanged | OnSwitchChanged? | - | 改变事件 |
 | openText | String? | - | 打开文案 |
 | size | TSwitchSize? | TSwitchSize.medium | 尺寸:大、中、小 |
@@ -240,4 +240,37 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | type | TSwitchType? | TSwitchType.fill | 类型:填充、文本、加载 |
 
 
+### TSwitchSize
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| large | - |
+| medium | - |
+| small | - |
+
+
+### TSwitchType
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| fill | - |
+| text | - |
+| loading | - |
+| icon | - |
+
+
+### OnSwitchChanged
+#### 简介
+开关改变事件处理
+#### 类型定义
+
+```dart
+typedef OnSwitchChanged = bool Function(bool value);
+```
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/tab-bar/README.md b/tdesign-site/src/tab-bar/README.md
index f17afd5e9..e60bc0c42 100644
--- a/tdesign-site/src/tab-bar/README.md
+++ b/tdesign-site/src/tab-bar/README.md
@@ -576,11 +576,11 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
+| basicType | TBottomTabBarBasicType | - | 基本样式(纯文本、纯图标、图标+文本) |
 | animationCurve | Curve | Curves.easeInOutCubic | 动画曲线 |
 | animationDuration | Duration | const Duration(milliseconds: 300) | 动画时长 |
 | backgroundColor | Color? | - | 背景颜色 (可选) |
 | barHeight | double? | _kDefaultTabBarHeight | tab高度 |
-| basicType | TBottomTabBarBasicType | basicType | 基本样式(纯文本、纯图标、图标+文本) |
 | centerDistance | double? | - | icon与文本中间距离(可选) |
 | componentType | TBottomTabBarComponentType? | TBottomTabBarComponentType.label | 选项样式 默认label |
 | currentIndex | int? | - | 选中的index(可选) |
@@ -588,7 +588,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | dividerHeight | double? | - | 分割线高度(可选) |
 | dividerThickness | double? | - | 分割线厚度(可选) |
 | indicatorAnimation | TBottomTabBarIndicatorAnimation | TBottomTabBarIndicatorAnimation.none | 指示器动画类型 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | navigationTabs | List | - | tabs配置 |
 | needInkWell | bool | false | 是否需要水波纹效果 |
 | outlineType | TBottomTabBarOutlineType? | TBottomTabBarOutlineType.filled | 标签栏样式 默认filled |
@@ -600,8 +600,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | useSafeArea | bool | true | 使用安全区域 |
 | useVerticalDivider | bool? | - | 是否使用竖线分隔(如果选项样式为 label,则强制为 false) |
 
-```
-```
 
 ### BadgeConfig
 #### 默认构造方法
@@ -613,8 +611,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | showBadge | bool | - | 是否展示消息 |
 | tBadge | TBadge? | - | 消息样式(未设置但 showBadge 为 true,则默认使用红点) |
 
-```
-```
 
 ### TBottomTabBarTabConfig
 #### 默认构造方法
@@ -632,8 +628,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | unselectedIcon | Widget? | - | 未选中时图标 |
 | unselectTabTextStyle | TextStyle? | - | 文本未选择样式 basicType为text时必填 |
 
-```
-```
 
 ### TBottomTabBarPopUpBtnConfig
 #### 默认构造方法
@@ -644,8 +638,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | onChanged | ValueChanged | - | 统一在 onChanged 中处理各item点击事件 |
 | popUpDialogConfig | TBottomTabBarPopUpShapeConfig? | - | 弹窗UI配置 |
 
-```
-```
 
 ### TBottomTabBarPopUpShapeConfig
 #### 默认构造方法
@@ -659,8 +651,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | popUpWidth | double? | - | 弹窗宽度(不设置,默认为按钮宽度 - 20) |
 | radius | double? | - | panel圆角 默认0 |
 
-```
-```
 
 ### PopUpMenuItem
 #### 默认构造方法
@@ -669,8 +659,51 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | --- | --- | --- | --- |
 | alignment | AlignmentGeometry | AlignmentDirectional.center | 对齐方式 |
 | itemWidget | Widget? | - | 选项widget |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | value | String | - | 选项值 |
 
 
+### TBottomTabBarBasicType
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| text | 单层级纯文本标签栏 |
+| iconText | 文本加图标标签栏 |
+| icon | 纯图标标签栏 |
+| expansionPanel | 双层级纯文本标签栏 |
+
+
+### TBottomTabBarComponentType
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| normal | 普通样式 |
+| label | 带胶囊背景的item选中样式 |
+
+
+### TBottomTabBarOutlineType
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| filled | 填充样式 |
+| capsule | 胶囊样式 |
+
+
+### TBottomTabBarIndicatorAnimation
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| none | 无动画,瞬间切换 |
+| linear | 线性滑动:指示器匀速从一个 tab 滑到另一个 |
+| elastic | 弹性动画:指示器先拉伸后收缩 |
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/table/README.md b/tdesign-site/src/table/README.md
index fc54bf5a3..8203fc29c 100644
--- a/tdesign-site/src/table/README.md
+++ b/tdesign-site/src/table/README.md
@@ -288,7 +288,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | empty | TTableEmpty? | - | 空表格呈现样式 |
 | footerWidget | Widget? | - | 自定义表尾 |
 | height | double? | - | 表格高度,超出后会出现滚动条 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | loading | bool? | false | 加载中状态 |
 | loadingWidget | Widget? | - | 自定义加载中状态 |
 | onCellTap | OnCellTap? | - | 单元格点击事件 |
@@ -300,8 +300,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | stripe | bool? | false | 斑马纹 |
 | width | double? | - | 表格宽度 |
 
-```
-```
 
 ### TTableCol
 #### 默认构造方法
@@ -321,8 +319,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | title | String? | - | 表头标题 |
 | width | double? | - | 列宽 |
 
-```
-```
 
 ### TTableEmpty
 #### 默认构造方法
@@ -333,4 +329,74 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | text | String? | - | 空状态文字 |
 
 
+### TTableColFixed
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| left | - |
+| right | - |
+| none | - |
+
+
+### TTableColAlign
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| left | - |
+| center | - |
+| right | - |
+
+
+### SelectableFunc
+#### 类型定义
+
+```dart
+typedef SelectableFunc = bool Function(int index, dynamic row);
+```
+
+
+### RowCheckFunc
+#### 类型定义
+
+```dart
+typedef RowCheckFunc = bool Function(int index, dynamic row);
+```
+
+
+### OnCellTap
+#### 类型定义
+
+```dart
+typedef OnCellTap = void Function(int rowIndex, dynamic row, TTableCol col);
+```
+
+
+### OnScroll
+#### 类型定义
+
+```dart
+typedef OnScroll = void Function(ScrollController controller);
+```
+
+
+### OnSelect
+#### 类型定义
+
+```dart
+typedef OnSelect = void Function(List? data);
+```
+
+
+### OnRowSelect
+#### 类型定义
+
+```dart
+typedef OnRowSelect = void Function(int index, bool checked);
+```
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/tabs/README.md b/tdesign-site/src/tabs/README.md
index a484905e1..99da59326 100644
--- a/tdesign-site/src/tabs/README.md
+++ b/tdesign-site/src/tabs/README.md
@@ -325,24 +325,22 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | indicatorPadding | EdgeInsets? | - | 引导padding |
 | indicatorWidth | double? | - | tabBar下标宽度 |
 | isScrollable | bool | false | 是否滚动 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | labelColor | Color? | - | tabBar 已选标签颜色 |
 | labelPadding | EdgeInsetsGeometry? | - | tab间距 |
 | labelStyle | TextStyle? | - | 已选label字体 |
-| onTap |  Function(int)? | - | 点击事件 |
+| onTap | Function(int)? | - | 点击事件 |
 | outlineType | TTabBarOutlineType | TTabBarOutlineType.filled | 选项卡样式 |
 | physics | ScrollPhysics? | - | 自定义滑动 |
 | selectedBgColor | Color? | - | 被选中背景色,只有outlineType为capsule时有效 |
 | showIndicator | bool | false | 是否展示引导控件 |
-| tabAlignment |  | - |  |
+| tabAlignment | TabAlignment? | - | - |
 | tabs | List | - | tab数组 |
 | unSelectedBgColor | Color? | - | 未选中背景色,只有outlineType为capsule时有效 |
 | unselectedLabelColor | Color? | - | tabBar未选标签颜色 |
 | unselectedLabelStyle | TextStyle? | - | unselectedLabel字体 |
 | width | double? | - | tabBar宽度 |
 
-```
-```
 
 ### TTab
 #### 默认构造方法
@@ -356,14 +354,12 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | height | double? | - | tab高度 |
 | icon | Widget? | - | 图标 |
 | iconMargin | EdgeInsetsGeometry | const EdgeInsets.only(bottom: 4.0, right: 4.0) | 图标间距 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | outlineType | TTabOutlineType | TTabOutlineType.filled | 选项卡样式 |
 | size | TTabSize | TTabSize.small | 选项卡尺寸 |
 | text | String? | - | 文字内容 |
 | textMargin | EdgeInsetsGeometry? | - | 中间内容宽度 |
 
-```
-```
 
 ### TTabBarView
 #### 默认构造方法
@@ -373,7 +369,39 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | children | List | - | 子widget列表 |
 | controller | TabController? | - | 控制器 |
 | isSlideSwitch | bool | false | 是否可以滑动切换 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
+
+
+### TTabSize
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| large | - |
+| small | - |
+
+
+### TTabOutlineType
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| filled | 填充样式 |
+| capsule | 胶囊样式 |
+| card | 卡片 |
+
+
+### TTabBarOutlineType
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| filled | 填充样式 |
+| capsule | 胶囊样式 |
+| card | 卡片 |
 
 
   
\ No newline at end of file
diff --git a/tdesign-site/src/tag/README.md b/tdesign-site/src/tag/README.md
index 172fa64f6..056f0bab4 100644
--- a/tdesign-site/src/tag/README.md
+++ b/tdesign-site/src/tag/README.md
@@ -507,6 +507,7 @@ Mark标签
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
+| text | String | - | 标签内容 |
 | backgroundColor | Color? | - | 背景颜色,优先级高于style的backgroundColor |
 | disable | bool | false | 是否为禁用状态 |
 | fixedWidth | double? | - | 标签的固定宽度 |
@@ -517,7 +518,7 @@ Mark标签
 | iconWidget | Widget? | - | 自定义图标内容,需自处理颜色 |
 | isLight | bool | false | 是否为浅色 |
 | isOutline | bool | false | 是否为描边类型,默认不是 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | needCloseIcon | bool | false | 关闭图标 |
 | onCloseTap | GestureTapCallback? | - | 关闭图标点击事件 |
 | overflow | TextOverflow? | - | 文字溢出处理 |
@@ -525,18 +526,16 @@ Mark标签
 | shape | TTagShape | TTagShape.square | 标签形状 |
 | size | TTagSize | TTagSize.medium | 标签大小 |
 | style | TTagStyle? | - | 标签样式 |
-| text | String | text | 标签内容 |
 | textColor | Color? | - | 文字颜色,优先级高于style的textColor |
 | theme | TTagTheme? | - | 主题 |
 
-```
-```
 
 ### TSelectTag
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
+| text | String | - | 标签内容 |
 | disableSelect | bool | false | 是否禁用选择 |
 | disableSelectStyle | TTagStyle? | - | 不可选标签样式 |
 | fixedWidth | double? | - | 标签的固定宽度 |
@@ -546,7 +545,7 @@ Mark标签
 | isLight | bool | false | 是否为浅色 |
 | isOutline | bool | false | 是否为描边类型,默认不是 |
 | isSelected | bool | false | 是否选中 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | needCloseIcon | bool | false | 关闭图标 |
 | onCloseTap | GestureTapCallback? | - | 关闭图标点击事件 |
 | onSelectChanged | ValueChanged? | - | 标签点击,选中状态改变时的回调 |
@@ -554,12 +553,9 @@ Mark标签
 | selectStyle | TTagStyle? | - | 选中的标签样式 |
 | shape | TTagShape | TTagShape.square | 标签形状 |
 | size | TTagSize | TTagSize.medium | 标签大小 |
-| text | String | text | 标签内容 |
 | theme | TTagTheme? | - | 主题 |
 | unSelectStyle | TTagStyle? | - | 未选中标签样式 |
 
-```
-```
 
 ### TTagStyle
 #### 默认构造方法
@@ -575,14 +571,92 @@ Mark标签
 | fontWeight | FontWeight? | - | 字体粗细 |
 | textColor | Color? | - | 文字颜色 |
 
+#### 公开属性
+
+| 属性 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| closeIconColor | Color? | - | 关闭图标颜色 |
+
 
 #### 工厂构造方法
 
-| 名称  | 说明 |
-| --- |  --- |
-| TTagStyle.generateDisableSelectStyle  | 根据主题生成禁用Tag样式 |
-| TTagStyle.generateFillStyleByTheme  | 根据主题生成填充Tag样式 |
-| TTagStyle.generateOutlineStyleByTheme  | 根据主题生成描边Tag样式 |
+##### TTagStyle.generateDisableSelectStyle
+
+根据主题生成禁用Tag样式
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | 上下文,方便获取主题内容 |
+| isLight | bool | - | - |
+| isOutline | bool | - | - |
+| shape | TTagShape | - | - |
+
+
+##### TTagStyle.generateFillStyleByTheme
+
+根据主题生成填充Tag样式
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | 上下文,方便获取主题内容 |
+| theme | TTagTheme? | - | - |
+| light | bool | - | - |
+| shape | TTagShape | - | - |
+
+
+##### TTagStyle.generateOutlineStyleByTheme
+
+根据主题生成描边Tag样式
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | 上下文,方便获取主题内容 |
+| theme | TTagTheme? | - | - |
+| light | bool | - | - |
+| shape | TTagShape | - | - |
+
+
+### TTagTheme
+#### 简介
+Tag展示类型
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| defaultTheme | 默认 |
+| primary | 常规 |
+| warning | 警告 |
+| danger | 危险 |
+| success | 成功 |
+
+
+### TTagSize
+#### 简介
+标签尺寸
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| extraLarge | - |
+| large | - |
+| medium | - |
+| small | - |
+| custom | - |
+
+
+### TTagShape
+#### 简介
+标签形状
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| square | - |
+| round | - |
+| mark | - |
 
 
   
\ No newline at end of file
diff --git a/tdesign-site/src/text/README.md b/tdesign-site/src/text/README.md
index 351b6c18e..a72595af3 100644
--- a/tdesign-site/src/text/README.md
+++ b/tdesign-site/src/text/README.md
@@ -199,8 +199,8 @@ TText.rich测试:
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
+| data | - | - | 以下系统 text 属性,释义请参考系统 [Text] 中注释 |
 | backgroundColor | Color? | - | 背景颜色 |
-| data | null | data | 以下系统 text 属性,释义请参考系统 [Text] 中注释 |
 | font | Font? | - | 字体尺寸,包含 大小size 和 行高height |
 | fontFamily | FontFamily? | - | 字体ttf |
 | fontFamilyUrl | String? | - | 是否禁用懒加载 FontFamily 的能力 |
@@ -208,66 +208,96 @@ TText.rich测试:
 | forceVerticalCenter | bool | false | 是否强制居中 |
 | isInFontLoader | bool | false | 是否在 FontLoader 中使用 |
 | isTextThrough | bool? | false | 是否是横线穿过样式(删除线) |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | lineThroughColor | Color? | - | 删除线颜色,对应 TestStyle 的 decorationColor |
-| locale |  | - |  |
-| maxLines |  | - |  |
-| overflow |  | - |  |
+| locale | Locale? | - | - |
+| maxLines | int? | - | - |
+| overflow | TextOverflow? | - | - |
 | package | String? | - | 字体包名 |
-| semanticsLabel |  | - |  |
-| softWrap |  | - |  |
-| strutStyle |  | - |  |
+| semanticsLabel | String? | - | - |
+| softWrap | bool? | - | - |
+| strutStyle | StrutStyle? | - | - |
 | style | TextStyle? | - | 自定义的 TextStyle,其中指定的属性,将覆盖扩展的外层属性 |
-| textAlign |  | - |  |
+| textAlign | TextAlign? | - | - |
 | textColor | Color? | - | 文本颜色 |
-| textDirection |  | - |  |
-| textHeightBehavior |  | - |  |
-| textScaleFactor |  | - |  |
-| textWidthBasis |  | - |  |
+| textDirection | TextDirection? | - | - |
+| textHeightBehavior | ui.TextHeightBehavior? | - | - |
+| textScaleFactor | double? | - | - |
+| textWidthBasis | TextWidthBasis? | - | - |
+
+#### 公开属性
+
+| 属性 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| textSpan | InlineSpan? | - | - |
 
 
 #### 工厂构造方法
 
-| 名称  | 说明 |
-| --- |  --- |
-| TText.rich  | 富文本构造方法 |
+##### TText.rich
+
+富文本构造方法
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| textSpan | InlineSpan? | - | - |
+| font | Font? | - | 字体尺寸,包含 大小size 和 行高height |
+| fontWeight | FontWeight? | - | 字体粗细 |
+| fontFamily | FontFamily? | - | 字体ttf |
+| textColor | Color? | - | 文本颜色 |
+| backgroundColor | Color? | - | 背景颜色 |
+| isTextThrough | bool? | false | 是否是横线穿过样式(删除线) |
+| lineThroughColor | Color? | - | 删除线颜色,对应 TestStyle 的 decorationColor |
+| package | String? | - | 字体包名 |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
+| style | TextStyle? | - | 自定义的 TextStyle,其中指定的属性,将覆盖扩展的外层属性 |
+| strutStyle | StrutStyle? | - | - |
+| textAlign | TextAlign? | - | - |
+| textDirection | TextDirection? | - | - |
+| locale | Locale? | - | - |
+| softWrap | bool? | - | - |
+| overflow | TextOverflow? | - | - |
+| textScaleFactor | double? | - | - |
+| maxLines | int? | - | - |
+| semanticsLabel | String? | - | - |
+| textWidthBasis | TextWidthBasis? | - | - |
+| textHeightBehavior | ui.TextHeightBehavior? | - | - |
+| forceVerticalCenter | bool | false | 是否强制居中 |
+| isInFontLoader | bool | false | 是否在 FontLoader 中使用 |
+| fontFamilyUrl | String? | - | 是否禁用懒加载 FontFamily 的能力 |
 
-```
-```
 
 ### TTextSpan
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| children |  | - |  |
-| context |  | - |  |
-| font |  | - |  |
-| fontFamily |  | - |  |
-| fontWeight |  | - |  |
-| isTextThrough |  | false |  |
-| lineThroughColor |  | - |  |
-| mouseCursor |  | - |  |
-| onEnter |  | - |  |
-| onExit |  | - |  |
-| package |  | - |  |
-| recognizer |  | - |  |
-| semanticsLabel |  | - |  |
-| style |  | - |  |
-| text |  | - |  |
-| textColor |  | - |  |
+| children | List? | - | - |
+| context | BuildContext? | - | - |
+| font | Font? | - | - |
+| fontFamily | FontFamily? | - | - |
+| fontWeight | FontWeight? | - | - |
+| isTextThrough | bool? | false | - |
+| lineThroughColor | Color? | - | - |
+| mouseCursor | MouseCursor? | - | - |
+| onEnter | PointerEnterEventListener? | - | - |
+| onExit | PointerExitEventListener? | - | - |
+| package | String? | - | - |
+| recognizer | GestureRecognizer? | - | - |
+| semanticsLabel | String? | - | - |
+| style | TextStyle? | - | - |
+| text | String? | - | - |
+| textColor | Color? | - | - |
 
-```
-```
 
 ### TTextConfiguration
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| child |  | - |  |
+| child | Widget | - | - |
 | globalFontFamily | FontFamily? | - | 全局字体,kTextNeedGlobalFontFamily=true 时生效 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | paddingConfig | TTextPaddingConfig? | - | forceVerticalCenter=true 时,内置 padding 配置 |
 
 
diff --git a/tdesign-site/src/textarea/README.md b/tdesign-site/src/textarea/README.md
index 110a484af..2de7d101d 100644
--- a/tdesign-site/src/textarea/README.md
+++ b/tdesign-site/src/textarea/README.md
@@ -271,7 +271,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | inputDecoration | InputDecoration? | - | 自定义输入框TextField组件样式 |
 | inputFormatters | List? | - | 显示输入内容,如限制长度(LengthLimitingTextInputFormatter(6)) |
 | inputType | TextInputType? | - | 键盘类型,数字、字母 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | label | String? | - | 输入框标题 |
 | labelIcon | Widget? | - | 输入框标题图标 |
 | labelStyle | TextStyle? | - | 左侧标签文本样式 |
@@ -298,4 +298,14 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | width | double? | - | 输入框宽度 |
 
 
+### TTextareaLayout
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| vertical | - |
+| horizontal | - |
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/time-counter/README.md b/tdesign-site/src/time-counter/README.md
index f7dec7c0a..d0eb22c69 100644
--- a/tdesign-site/src/time-counter/README.md
+++ b/tdesign-site/src/time-counter/README.md
@@ -620,9 +620,9 @@ TTimeCounter _buildCustomUnitLargeSize(BuildContext context) {
 | controller | TTimeCounterController? | - | 控制器,可控制开始/暂停/继续/重置 |
 | direction | TTimeCounterDirection | TTimeCounterDirection.down | 计时方向,默认倒计时 |
 | format | String | 'HH:mm:ss' | 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒(分隔符必须为长度为1的非空格的字符) |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | millisecond | bool | false | 是否开启毫秒级渲染 |
-| onChange |  Function(int time)? | - | 时间变化时触发回调 |
+| onChange | Function(int time)? | - | 时间变化时触发回调 |
 | onFinish | VoidCallback? | - | 计时结束时触发回调 |
 | size | TTimeCounterSize | TTimeCounterSize.medium | 尺寸 |
 | splitWithUnit | bool | false | 使用时间单位分割 |
@@ -630,14 +630,10 @@ TTimeCounter _buildCustomUnitLargeSize(BuildContext context) {
 | theme | TTimeCounterTheme | TTimeCounterTheme.defaultTheme | 风格 |
 | time | int | - | 必需;计时时长,单位毫秒 |
 
-```
-```
 
 ### TTimeCounterController
 #### 简介
 倒计时组件控制器,可控制开始(`start()`)/暂停(`pause()`)/继续(`resume()`)/重置(`reset([int? time])`)
-```
-```
 
 ### TTimeCounterStyle
 #### 简介
@@ -665,9 +661,69 @@ TTimeCounter _buildCustomUnitLargeSize(BuildContext context) {
 
 #### 工厂构造方法
 
-| 名称  | 说明 |
-| --- |  --- |
-| TTimeCounterStyle.generateStyle  | 生成默认样式 |
+##### TTimeCounterStyle.generateStyle
+
+生成默认样式
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | - |
+| size | TTimeCounterSize? | - | - |
+| theme | TTimeCounterTheme? | - | - |
+| splitWithUnit | bool? | - | - |
+
+
+### TTimeCounterStatus
+#### 简介
+计时组件控制器转态
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| start | 开始 |
+| pause | 暂停 |
+| resume | 继续 |
+| reset | 重置 |
+| idle | 空,默认值 |
+
+
+### TTimeCounterDirection
+#### 简介
+计时组件计时方向
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| down | 倒计时 |
+| up | 正向计时 |
+
+
+### TTimeCounterSize
+#### 简介
+计时组件尺寸
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| small | 小 |
+| medium | 中等 |
+| large | 大 |
+
+
+### TTimeCounterTheme
+#### 简介
+计时组件风格
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| defaultTheme | 默认 |
+| round | 圆形 |
+| square | 方形 |
 
 
   
\ No newline at end of file
diff --git a/tdesign-site/src/toast/README.md b/tdesign-site/src/toast/README.md
index 4556b339e..9b836d4a5 100644
--- a/tdesign-site/src/toast/README.md
+++ b/tdesign-site/src/toast/README.md
@@ -386,18 +386,179 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 #### 静态方法
 
-| 名称 | 返回类型 | 参数 | 说明 |
+##### TToast.dismissAll
+
+关闭所有Toast
+
+返回类型:`void`
+
+##### TToast.dismissLoading
+
+关闭加载Toast(向后兼容)
+
+返回类型:`void`
+
+##### TToast.dismissToast
+
+关闭指定的Toast
+
+返回类型:`void`
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| toastId | String | - | - |
+
+
+##### TToast.showFail
+
+失败提示Toast
+
+返回类型:`String`
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| text | String? | - | - |
+| direction | IconTextDirection | IconTextDirection.horizontal | - |
+| context | BuildContext | - | - |
+| duration | Duration | const Duration(milliseconds: 3000) | - |
+| preventTap | bool? | - | - |
+| backgroundColor | Color? | - | - |
+| maxLines | int? | - | - |
+| textStyle | TextStyle? | - | - |
+| iconSize | double? | - | - |
+| iconColor | Color? | - | - |
+| toastId | String? | - | - |
+
+
+##### TToast.showIconText
+
+带图标的Toast
+
+返回类型:`String`
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| text | String? | - | - |
+| icon | IconData? | - | - |
+| direction | IconTextDirection | IconTextDirection.horizontal | - |
+| context | BuildContext | - | - |
+| duration | Duration | const Duration(milliseconds: 3000) | - |
+| preventTap | bool? | - | - |
+| backgroundColor | Color? | - | - |
+| maxLines | int? | - | - |
+| textStyle | TextStyle? | - | - |
+| iconSize | double? | - | - |
+| iconColor | Color? | - | - |
+| toastId | String? | - | - |
+
+
+##### TToast.showLoading
+
+带文案的加载Toast
+
+返回类型:`String`
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | - |
+| text | String? | - | - |
+| duration | Duration | const Duration(seconds: 99999999) | - |
+| preventTap | bool? | - | - |
+| customWidget | Widget? | - | - |
+| backgroundColor | Color? | - | - |
+| textStyle | TextStyle? | - | - |
+| iconSize | double? | - | - |
+| iconColor | Color? | - | - |
+| toastId | String? | - | - |
+
+
+##### TToast.showLoadingWithoutText
+
+不带文案的加载Toast
+
+返回类型:`String`
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | - |
+| duration | Duration | const Duration(seconds: 99999999) | - |
+| preventTap | bool? | - | - |
+| backgroundColor | Color? | - | - |
+| iconSize | double? | - | - |
+| iconColor | Color? | - | - |
+| toastId | String? | - | - |
+
+
+##### TToast.showSuccess
+
+成功提示Toast
+
+返回类型:`String`
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| text | String? | - | - |
+| direction | IconTextDirection | IconTextDirection.horizontal | - |
+| context | BuildContext | - | - |
+| duration | Duration | const Duration(milliseconds: 3000) | - |
+| preventTap | bool? | - | - |
+| backgroundColor | Color? | - | - |
+| maxLines | int? | - | - |
+| textStyle | TextStyle? | - | - |
+| iconSize | double? | - | - |
+| iconColor | Color? | - | - |
+| toastId | String? | - | - |
+
+
+##### TToast.showText
+
+普通文本Toast
+
+返回类型:`String`
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| text | String? | - | - |
+| context | BuildContext | - | - |
+| duration | Duration | const Duration(milliseconds: 3000) | - |
+| maxLines | int? | - | - |
+| constraints | BoxConstraints? | - | - |
+| preventTap | bool? | - | - |
+| customWidget | Widget? | - | - |
+| backgroundColor | Color? | - | - |
+| textStyle | TextStyle? | - | - |
+| toastId | String? | - | - |
+
+
+##### TToast.showWarning
+
+警告Toast
+
+返回类型:`String`
+
+| 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| dismissAll |  |  | 关闭所有Toast |
-| dismissLoading |  |  | 关闭加载Toast(向后兼容) |
-| dismissToast |  |   required String toastId, | 关闭指定的Toast |
-| showFail |  |   required String? text,  IconTextDirection direction,  required BuildContext context,  Duration duration,  bool? preventTap,  Color? backgroundColor,  int? maxLines,  TextStyle? textStyle,  double? iconSize,  Color? iconColor,  String? toastId, | 失败提示Toast |
-| showIconText |  |   required String? text,  IconData? icon,  IconTextDirection direction,  required BuildContext context,  Duration duration,  bool? preventTap,  Color? backgroundColor,  int? maxLines,  TextStyle? textStyle,  double? iconSize,  Color? iconColor,  String? toastId, | 带图标的Toast |
-| showLoading |  |   required BuildContext context,  String? text,  Duration duration,  bool? preventTap,  Widget? customWidget,  Color? backgroundColor,  TextStyle? textStyle,  double? iconSize,  Color? iconColor,  String? toastId, | 带文案的加载Toast |
-| showLoadingWithoutText |  |   required BuildContext context,  Duration duration,  bool? preventTap,  Color? backgroundColor,  double? iconSize,  Color? iconColor,  String? toastId, | 不带文案的加载Toast |
-| showSuccess |  |   required String? text,  IconTextDirection direction,  required BuildContext context,  Duration duration,  bool? preventTap,  Color? backgroundColor,  int? maxLines,  TextStyle? textStyle,  double? iconSize,  Color? iconColor,  String? toastId, | 成功提示Toast |
-| showText |  |   required String? text,  required BuildContext context,  Duration duration,  int? maxLines,  BoxConstraints? constraints,  bool? preventTap,  Widget? customWidget,  Color? backgroundColor,  TextStyle? textStyle,  String? toastId, | 普通文本Toast |
-| showWarning |  |   required String? text,  IconTextDirection direction,  required BuildContext context,  Duration duration,  bool? preventTap,  Color? backgroundColor,  int? maxLines,  TextStyle? textStyle,  double? iconSize,  Color? iconColor,  String? toastId, | 警告Toast |
+| text | String? | - | - |
+| direction | IconTextDirection | IconTextDirection.horizontal | - |
+| context | BuildContext | - | - |
+| duration | Duration | const Duration(milliseconds: 3000) | - |
+| preventTap | bool? | - | - |
+| backgroundColor | Color? | - | - |
+| maxLines | int? | - | - |
+| textStyle | TextStyle? | - | - |
+| iconSize | double? | - | - |
+| iconColor | Color? | - | - |
+| toastId | String? | - | - |
+
+
+### IconTextDirection
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| horizontal | 横向 |
+| vertical | 竖向 |
 
 
   
\ No newline at end of file
diff --git a/tdesign-site/src/tree-select/README.md b/tdesign-site/src/tree-select/README.md
index dbb2ecb37..0ddcb23ca 100644
--- a/tdesign-site/src/tree-select/README.md
+++ b/tdesign-site/src/tree-select/README.md
@@ -193,15 +193,13 @@ String类型ID(问题3)
 | --- | --- | --- | --- |
 | defaultValue | List | const [] | 初始值,对应options中的value值 |
 | height | double | 336 | 高度 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | multiple | bool | false | 支持多选 |
 | onChange | TTreeSelectChangeEvent? | - | 选中值发生变化 |
 | options | List | const [] | 展示的选项列表 |
 | outwardCornerRadius | double | 9 | 一级菜单选中项的外弯折圆角半径,默认为 9 |
 | style | TTreeSelectStyle | TTreeSelectStyle.normal | 一级菜单样式 |
 
-```
-```
 
 ### TSelectOption
 #### 默认构造方法
@@ -216,4 +214,22 @@ String类型ID(问题3)
 | value | dynamic | - | 值 |
 
 
+### TTreeSelectStyle
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| normal | - |
+| outline | - |
+
+
+### TTreeSelectChangeEvent
+#### 类型定义
+
+```dart
+typedef TTreeSelectChangeEvent = void Function(List, int level);
+```
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/src/upload/README.md b/tdesign-site/src/upload/README.md
index f0ae803a5..005a474df 100644
--- a/tdesign-site/src/upload/README.md
+++ b/tdesign-site/src/upload/README.md
@@ -187,7 +187,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | enabledReplaceType | bool? | false | 是否启用replace功能 |
 | files | List | - | 控制展示的文件列表 |
 | height | double? | 80.0 | 图片高度 |
-| key |  | - |  |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | max | int | 0 | 用于控制文件上传数量,0为不限制,仅在multiple为true时有效 |
 | mediaType | List | const [TUploadMediaType.image, TUploadMediaType.video] | 支持上传的文件类型,图片或视频 |
 | multiple | bool | false | 是否多选上传,默认false |
@@ -206,4 +206,89 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | wrapSpacing | double? | - | 多图布局时的 spacing |
 
 
+### TUploadMediaType
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| image | - |
+| video | - |
+
+
+### TUploadValidatorError
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| overSize | - |
+| overQuantity | - |
+
+
+### TUploadFileStatus
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| success | - |
+| loading | - |
+| error | - |
+| retry | - |
+
+
+### TUploadType
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| add | - |
+| remove | - |
+| replace | - |
+
+
+### TUploadBoxType
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| roundedSquare | - |
+| circle | - |
+
+
+### TUploadErrorEvent
+#### 类型定义
+
+```dart
+typedef TUploadErrorEvent = void Function(Object e);
+```
+
+
+### TUploadClickEvent
+#### 类型定义
+
+```dart
+typedef TUploadClickEvent = void Function(int value);
+```
+
+
+### TUploadValueChangedEvent
+#### 类型定义
+
+```dart
+typedef TUploadValueChangedEvent = void Function(List files, TUploadType type);
+```
+
+
+### TUploadValidatorEvent
+#### 类型定义
+
+```dart
+typedef TUploadValidatorEvent = void Function(TUploadValidatorError e);
+```
+
+
   
\ No newline at end of file
diff --git a/tdesign-site/template.config.js b/tdesign-site/template.config.js
deleted file mode 100644
index 60402298a..000000000
--- a/tdesign-site/template.config.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * This file is a configuration file generated by the `Template` extension on `vscode`
- * @see https://marketplace.visualstudio.com/items?itemName=yongwoo.template
- */
-module.exports = {
-  // You can change the template path to another path
-  templateRootPath: "./.templates",
-  // After copying the template file the `replaceFileTextFn` function is executed
-  replaceFileTextFn: (fileText, templateName, utils) => {
-    // @see https://www.npmjs.com/package/change-case
-    const { changeCase } = utils;
-    // You can change the text in the file
-    return fileText
-      .replace(/__templateName__/g, templateName)
-      .replace(
-        /__templateNameToPascalCase__/g,
-        changeCase.pascalCase(templateName)
-      )
-      .replace(
-        /__templateNameToParamCase__/g,
-        changeCase.paramCase(templateName)
-      );
-  },
-  renameFileFn: (fileName, templateName, utils) => {
-    const { path } = utils;
-    const { base } = path.parse(fileName);
-    return base.replace(/__templateName__/gm, templateName);
-  },
-  renameSubDirectoriesFn: (directoryName, templateName, _utils) => {
-    const { changeCase } = _utils;
-    const newDirectoryName = changeCase.paramCase(templateName);
-    return directoryName.replace(/__templateName__/g, newDirectoryName);
-  }
-};

From 2f1c7aa7c07f4c02f6abc48001c11d7444b10566 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= 
Date: Wed, 27 May 2026 01:07:25 +0800
Subject: [PATCH 33/35] refactor(calendar): remove draggable option from
 TCalendar and update documentation for showPopup method

---
 tdesign-component/example/assets/api/calendar_api.md          | 2 +-
 tdesign-component/lib/src/components/calendar/t_calendar.dart | 3 ---
 tdesign-site/src/calendar/README.md                           | 2 +-
 3 files changed, 2 insertions(+), 5 deletions(-)

diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md
index e23fa0c7f..122cffb0f 100644
--- a/tdesign-component/example/assets/api/calendar_api.md
+++ b/tdesign-component/example/assets/api/calendar_api.md
@@ -32,7 +32,7 @@
 
 | 名称 | 返回类型 | 参数 | 说明 |
 | --- | --- | --- | --- |
-| showPopup |  |   required BuildContext context,  Widget? titleWidget,  CalendarType type,  List? initialValue,  DateTime? minDate,  DateTime? maxDate,  DateTime? anchorDate,  int anchorRevision,  double? popupHeight,  int firstDayOfWeek,  double? cellHeight,  TCalendarStyle? style,  Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder,  ValueListenable? popupBottomExpanded,  Widget Function(VoidCallback onConfirm)? confirmBtnBuilder,  void Function(List)? onConfirm,  VoidCallback? onClose,  void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick,  bool autoClose,  bool draggable,  TCalendarCellBuilder? cellBuilder,  TCalendarSubtitleBuilder? subtitleBuilder,  TCalendarDataSource? dataSource,  ValueChanged? onMonthChange,  Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。     取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。   弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`,   或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。     ```dart   final result = await TCalendar.showPopup(     context,     titleWidget: Text('请选择日期'),     type: CalendarType.single,   );   if (result != null) {     print('选中了: $result');   }   ```     若需完全自定义布局,请直接使用 [TCalendar] + [TPopup.show]   / [TPopupOptions.bottom] 自行组装。 |
+| showPopup |  |   required BuildContext context,  Widget? titleWidget,  CalendarType type,  List? initialValue,  DateTime? minDate,  DateTime? maxDate,  DateTime? anchorDate,  int anchorRevision,  double? popupHeight,  int firstDayOfWeek,  double? cellHeight,  TCalendarStyle? style,  Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder,  ValueListenable? popupBottomExpanded,  Widget Function(VoidCallback onConfirm)? confirmBtnBuilder,  void Function(List)? onConfirm,  VoidCallback? onClose,  void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick,  bool autoClose,  TCalendarCellBuilder? cellBuilder,  TCalendarSubtitleBuilder? subtitleBuilder,  TCalendarDataSource? dataSource,  ValueChanged? onMonthChange,  Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。     取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。   弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`,   或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。     ```dart   final result = await TCalendar.showPopup(     context,     titleWidget: Text('请选择日期'),     type: CalendarType.single,   );   if (result != null) {     print('选中了: $result');   }   ```     若需完全自定义布局,请直接使用 [TCalendar] + [TPopup.show]   / [TPopupOptions.bottom] 自行组装。 |
 
 ```
 ```
diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart
index 2898b14bc..2d53b4aff 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart
@@ -340,9 +340,6 @@ class TCalendar extends StatefulWidget {
     /// 点击遮罩或物理返回是否关闭
     bool autoClose = true,
 
-    /// 面板是否可拖动
-    bool draggable = false,
-
     TCalendarCellBuilder? cellBuilder,
     TCalendarSubtitleBuilder? subtitleBuilder,
     TCalendarDataSource? dataSource,
diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md
index 7cb361fb1..ca0815153 100644
--- a/tdesign-site/src/calendar/README.md
+++ b/tdesign-site/src/calendar/README.md
@@ -465,7 +465,7 @@ Widget _buildLunar(BuildContext context) {
 
 | 名称 | 返回类型 | 参数 | 说明 |
 | --- | --- | --- | --- |
-| showPopup |  |   required BuildContext context,  Widget? titleWidget,  CalendarType type,  List? initialValue,  DateTime? minDate,  DateTime? maxDate,  DateTime? anchorDate,  int anchorRevision,  double? popupHeight,  int firstDayOfWeek,  double? cellHeight,  TCalendarStyle? style,  Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder,  ValueListenable? popupBottomExpanded,  Widget Function(VoidCallback onConfirm)? confirmBtnBuilder,  void Function(List)? onConfirm,  VoidCallback? onClose,  void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick,  bool autoClose,  bool draggable,  TCalendarCellBuilder? cellBuilder,  TCalendarSubtitleBuilder? subtitleBuilder,  TCalendarDataSource? dataSource,  ValueChanged? onMonthChange,  Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。     取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。   弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`,   或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。     ```dart   final result = await TCalendar.showPopup(     context,     titleWidget: Text('请选择日期'),     type: CalendarType.single,   );   if (result != null) {     print('选中了: $result');   }   ```     若需完全自定义布局,请直接使用 [TCalendar] + [TPopup.show] / [TPopupOptions.bottom] 自行组装。 |
+| showPopup |  |   required BuildContext context,  Widget? titleWidget,  CalendarType type,  List? initialValue,  DateTime? minDate,  DateTime? maxDate,  DateTime? anchorDate,  int anchorRevision,  double? popupHeight,  int firstDayOfWeek,  double? cellHeight,  TCalendarStyle? style,  Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder,  ValueListenable? popupBottomExpanded,  Widget Function(VoidCallback onConfirm)? confirmBtnBuilder,  void Function(List)? onConfirm,  VoidCallback? onClose,  void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick,  bool autoClose,  TCalendarCellBuilder? cellBuilder,  TCalendarSubtitleBuilder? subtitleBuilder,  TCalendarDataSource? dataSource,  ValueChanged? onMonthChange,  Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。     取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。   弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`,   或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。     ```dart   final result = await TCalendar.showPopup(     context,     titleWidget: Text('请选择日期'),     type: CalendarType.single,   );   if (result != null) {     print('选中了: $result');   }   ```     若需完全自定义布局,请直接使用 [TCalendar] + [TPopup.show] / [TPopupOptions.bottom] 自行组装。 |
 
 ```
 ```

From d8ffac1331f319226c9a894a5cac5bb470a18512 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= 
Date: Wed, 27 May 2026 14:38:22 +0800
Subject: [PATCH 34/35] refactor(calendar): rename popupBottomBuilder and
 popupBottomExpanded to popupOverlayBuilder and popupOverlayExpanded, update
 documentation and examples accordingly

---
 .../example/assets/api/calendar_api.md        |   6 +-
 .../example/lib/page/t_calendar_page.dart     | 379 +-----------------
 .../src/components/calendar/t_calendar.dart   | 118 +++---
 .../components/calendar/t_calendar_body.dart  |  33 +-
 .../calendar/t_calendar_header.dart           |  31 +-
 tdesign-component/test/t_calendar_test.dart   |  28 +-
 tdesign-site/src/calendar/README.md           |   6 +-
 7 files changed, 132 insertions(+), 469 deletions(-)

diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md
index 122cffb0f..51fc197f5 100644
--- a/tdesign-component/example/assets/api/calendar_api.md
+++ b/tdesign-component/example/assets/api/calendar_api.md
@@ -32,7 +32,7 @@
 
 | 名称 | 返回类型 | 参数 | 说明 |
 | --- | --- | --- | --- |
-| showPopup |  |   required BuildContext context,  Widget? titleWidget,  CalendarType type,  List? initialValue,  DateTime? minDate,  DateTime? maxDate,  DateTime? anchorDate,  int anchorRevision,  double? popupHeight,  int firstDayOfWeek,  double? cellHeight,  TCalendarStyle? style,  Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder,  ValueListenable? popupBottomExpanded,  Widget Function(VoidCallback onConfirm)? confirmBtnBuilder,  void Function(List)? onConfirm,  VoidCallback? onClose,  void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick,  bool autoClose,  TCalendarCellBuilder? cellBuilder,  TCalendarSubtitleBuilder? subtitleBuilder,  TCalendarDataSource? dataSource,  ValueChanged? onMonthChange,  Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。     取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。   弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`,   或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。     ```dart   final result = await TCalendar.showPopup(     context,     titleWidget: Text('请选择日期'),     type: CalendarType.single,   );   if (result != null) {     print('选中了: $result');   }   ```     若需完全自定义布局,请直接使用 [TCalendar] + [TPopup.show]   / [TPopupOptions.bottom] 自行组装。 |
+| showPopup |  |   required BuildContext context,  Widget? titleWidget,  CalendarType type,  List? initialValue,  DateTime? minDate,  DateTime? maxDate,  DateTime? anchorDate,  int anchorRevision,  double? popupHeight,  int firstDayOfWeek,  double? cellHeight,  TCalendarStyle? style,  Widget Function(BuildContext context, List selectedDates)? popupOverlayBuilder,  ValueListenable? popupOverlayExpanded,  Widget Function(VoidCallback onConfirm)? confirmBtnBuilder,  void Function(List)? onConfirm,  VoidCallback? onClose,  void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick,  TCalendarCellBuilder? cellBuilder,  TCalendarSubtitleBuilder? subtitleBuilder,  TCalendarDataSource? dataSource,  ValueChanged? onMonthChange,  Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。     取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。   弹窗内点选过程无 [onChange];实时联动请用 [popupOverlayBuilder] 的 `dates`,   或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。     ```dart   final result = await TCalendar.showPopup(     context,     titleWidget: Text('请选择日期'),     type: CalendarType.single,   );   if (result != null) {     print('选中了: $result');   }   ```     若需完全自定义布局,请直接使用 [TCalendar] + [TPopup.show]   / [TPopupOptions.bottom] 自行组装。 |
 
 ```
 ```
@@ -47,8 +47,8 @@
 | key |  | - |  |
 | onClose |  | - |  |
 | onConfirm |  | - |  |
-| popupBottomBuilder | Widget Function(BuildContext context, List selectedDates)? | - | 弹窗底部自定义区域构建器(仅弹窗模式,由 [TCalendar.showPopup] 或手动 |
-| popupBottomExpanded | ValueListenable? | - | 弹窗底部区域是否展开(响应式),需配合 [popupBottomBuilder]。 |
+| popupOverlayBuilder | Widget Function(BuildContext context, List selectedDates)? | - | 弹窗模式下日历内容区底部浮层构建器(非 TPopup 面板底部),由 [TCalendar.showPopup] 或手动 |
+| popupOverlayExpanded | ValueListenable? | - | 浮层是否展开(响应式),需配合 [popupOverlayBuilder]。 |
 | popupConfirmBtn | bool? | - | 是否由 [TCalendar] 渲染底部确认按钮。 |
 | popupControls | bool | true | 是否由 [TCalendar] 自行渲染关闭按钮和标题行。 |
 | selected | ValueNotifier> | - | 选中态的可写引用(仅供 [TCalendar] 内部更新使用)。 |
diff --git a/tdesign-component/example/lib/page/t_calendar_page.dart b/tdesign-component/example/lib/page/t_calendar_page.dart
index 19d430e2e..2391132f0 100644
--- a/tdesign-component/example/lib/page/t_calendar_page.dart
+++ b/tdesign-component/example/lib/page/t_calendar_page.dart
@@ -8,9 +8,8 @@ import '../lunar_data_source_example.dart';
 ///
 /// 演示 [TCalendar] 的所有使用方式:
 /// - **Popup 模式**:通过 [TCalendar.showPopup] 以弹窗形式展示日历
-///   - 单选、多选、区间选择
-///   - 单选 + 时间、区间 + 时间
-///   - 锚点定位
+///   - 单选、多选、区间选择、锚点定位
+///   - `popupOverlayBuilder` / `popupOverlayExpanded`(弹窗模式日历内容区底部浮层)
 /// - **自定义样式**:文案、按钮、日期单元格
 /// - **农历日历**:结合 [TCalendarDataSource] 展示农历信息
 class TCalendarPage extends StatelessWidget {
@@ -63,13 +62,11 @@ Widget _buildSimple(BuildContext context) {
 
 /// 「组件类型」演示容器
 ///
-/// 包含 6 种 [TCalendar.showPopup] 弹窗模式:
-/// 1. 单选 + 天气 bottom
+/// 包含 4 种 [TCalendar.showPopup] 弹窗模式:
+/// 1. 单选 + 天气浮层(演示 popupOverlayBuilder / popupOverlayExpanded)
 /// 2. 多选 + 已选汇总 bottom
 /// 3. 区间选择 + 区间摘要 bottom
-/// 4. 单选 + 时间选择器 bottom
-/// 5. 区间 + 双时间选择器 bottom(Tab 切换开始/结束)
-/// 6. 锚点定位到指定月份
+/// 4. 锚点定位到指定月份
 class _SimpleDemo extends StatelessWidget {
   const _SimpleDemo();
 
@@ -80,8 +77,6 @@ class _SimpleDemo extends StatelessWidget {
         _SingleCalendarCell(),
         _MultipleCalendarCell(),
         _RangeCalendarCell(),
-        _SingleTimeCalendarCell(),
-        _RangeTimeCalendarCell(),
         _AnchorCalendarCell(),
       ],
     );
@@ -91,7 +86,7 @@ class _SimpleDemo extends StatelessWidget {
 // ========================= 1. 单选 + 天气 =========================
 /// 单选日历 + bottom 天气面板
 ///
-/// 演示 [TCalendar.showPopup] 的 popupBottomBuilder / popupBottomExpanded 用法
+/// 演示 [TCalendar.showPopup] 的 popupOverlayBuilder / popupOverlayExpanded 用法
 /// (底部区域仅弹窗模式,经 [TCalendarInherited] 注入,不可传给内嵌 [TCalendar]):
 /// - 选中日期后展开 bottom 区域显示天气信息
 /// - 确认后回传选中值
@@ -129,9 +124,9 @@ class _SingleCalendarCellState extends State<_SingleCalendarCell> {
           context,
           titleWidget: const Text('请选择日期'),
           initialValue: _selected,
-          popupBottomExpanded: _expanded,
+          popupOverlayExpanded: _expanded,
           onCellClick: (value, selectType, tdate) => _expanded.value = true,
-          popupBottomBuilder: (bCtx, dates) {
+          popupOverlayBuilder: (bCtx, dates) {
             final d = dates.isEmpty ? DateTime.now() : dates.first;
             return _WeatherPanel(date: d, weather: _weatherFor(d));
           },
@@ -146,7 +141,7 @@ class _SingleCalendarCellState extends State<_SingleCalendarCell> {
 // ========================= 2. 多选 =========================
 /// 多选日历 + bottom 已选汇总
 ///
-/// 演示 [CalendarType.multiple] 多选模式,popupBottomBuilder 区域展示已选日期列表。
+/// 演示 [CalendarType.multiple] 多选模式,popupOverlayBuilder 区域展示已选日期列表。
 class _MultipleCalendarCell extends StatefulWidget {
   const _MultipleCalendarCell();
   @override
@@ -168,7 +163,7 @@ class _MultipleCalendarCellState extends State<_MultipleCalendarCell> {
           titleWidget: const Text('请选择日期'),
           type: CalendarType.multiple,
           initialValue: _dates.isEmpty ? [DateTime.now()] : _dates,
-          popupBottomBuilder: (bCtx, dates) => _MultipleSummary(selected: dates),
+          popupOverlayBuilder: (bCtx, dates) => _MultipleSummary(selected: dates),
           onConfirm: (value) => setState(() => _dates = value),
         );
       },
@@ -179,7 +174,7 @@ class _MultipleCalendarCellState extends State<_MultipleCalendarCell> {
 // ========================= 3. 区间 =========================
 /// 区间选择日历 + bottom 区间摘要
 ///
-/// 演示 [CalendarType.range] 区间模式,popupBottomBuilder 区域展示开始/结束日期及天数。
+/// 演示 [CalendarType.range] 区间模式,popupOverlayBuilder 区域展示开始/结束日期及天数。
 class _RangeCalendarCell extends StatefulWidget {
   const _RangeCalendarCell();
   @override
@@ -206,7 +201,7 @@ class _RangeCalendarCellState extends State<_RangeCalendarCell> {
           titleWidget: const Text('请选择日期区间'),
           type: CalendarType.range,
           initialValue: _dates,
-          popupBottomBuilder: (bCtx, dates) => _RangeSummary(selected: dates),
+          popupOverlayBuilder: (bCtx, dates) => _RangeSummary(selected: dates),
           onConfirm: (value) => setState(() => _dates = value),
         );
       },
@@ -214,187 +209,7 @@ class _RangeCalendarCellState extends State<_RangeCalendarCell> {
   }
 }
 
-// ========================= 4. 单选 + 时间 =========================
-/// 单选日历 + 时间选择器
-///
-/// 演示 popupBottomBuilder 区域放置 [TPicker] 时间选择器,
-/// 确认时将日期和时间合并为完整 DateTime。
-class _SingleTimeCalendarCell extends StatefulWidget {
-  const _SingleTimeCalendarCell();
-  @override
-  State<_SingleTimeCalendarCell> createState() =>
-      _SingleTimeCalendarCellState();
-}
-
-class _SingleTimeCalendarCellState extends State<_SingleTimeCalendarCell> {
-  late List _selected;
-  // 当前选中的时分(持久跨弹窗)
-  late int _hour;
-  late int _minute;
-
-  @override
-  void initState() {
-    super.initState();
-    final now = DateTime.now();
-    _hour = now.hour;
-    _minute = now.minute;
-    // 初始值就带上当前时分
-    _selected = [
-      DateTime.now()
-          .add(const Duration(days: 30))
-          .copyWith(hour: _hour, minute: _minute, second: 0, millisecond: 0),
-    ];
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final d = _selected.first;
-    return TCell(
-      title: '单个选择日历和时间',
-      arrow: true,
-      note: '${d.year}-${d.month.toString().padLeft(2, '0')}-'
-          '${d.day.toString().padLeft(2, '0')} '
-          '${d.hour.toString().padLeft(2, '0')}:'
-          '${d.minute.toString().padLeft(2, '0')}',
-      onClick: (_) {
-        var popupHour = _hour;
-        var popupMinute = _minute;
-        TCalendar.showPopup(
-          context,
-          titleWidget: const Text('请选择日期和时间'),
-          popupHeight: 780,
-          initialValue: _selected,
-          popupBottomBuilder: (_, __) => _TimePickerPanel(
-            initialHour: popupHour,
-            initialMinute: popupMinute,
-            title: '选择时间',
-            onChange: (hour, min) {
-              popupHour = hour;
-              popupMinute = min;
-            },
-          ),
-          onConfirm: (dates) {
-            final merged = dates.map((d) {
-              return d.copyWith(
-                hour: popupHour,
-                minute: popupMinute,
-                second: 0,
-                millisecond: 0,
-              );
-            }).toList();
-            setState(() {
-              _selected = merged;
-              _hour = popupHour;
-              _minute = popupMinute;
-            });
-          },
-        );
-      },
-    );
-  }
-}
-
-// ========================= 5. 区间 + 时间 =========================
-/// 区间选择 + 双时间选择器
-///
-/// 演示 popupBottomBuilder 区域使用 [TTabBar] + [TPicker] 组合,
-/// 点击日期单元格时自动切换开始/结束时间 Tab,
-/// 确认时分别合并开始和结束的日期+时间。
-class _RangeTimeCalendarCell extends StatefulWidget {
-  const _RangeTimeCalendarCell();
-  @override
-  State<_RangeTimeCalendarCell> createState() => _RangeTimeCalendarCellState();
-}
-
-class _RangeTimeCalendarCellState extends State<_RangeTimeCalendarCell> {
-  late List _dates;
-  // 持久跨弹窗的开始/结束时分
-  late List _startTime;
-  late List _endTime;
-
-  @override
-  void initState() {
-    super.initState();
-    final now = DateTime.now();
-    _startTime = [now.hour, now.minute];
-    _endTime = [now.hour, now.minute];
-    // 初始值就带上当前时分
-    _dates = [
-      now.copyWith(second: 0, millisecond: 0),
-      now
-          .add(const Duration(days: 3))
-          .copyWith(second: 0, millisecond: 0),
-    ];
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return TCell(
-      title: '区间选择日历和时间',
-      arrow: true,
-      note: _dates.length >= 2
-          ? '${_formatMdHm(_dates.first)} ~ ${_formatMdHm(_dates.last)}'
-          : '--',
-      onClick: (_) {
-        var popupStartTime = List.from(_startTime);
-        var popupEndTime = List.from(_endTime);
-        final panelKey = GlobalKey<_RangeTimePickerPanelState>();
-        TCalendar.showPopup(
-          context,
-          titleWidget: const Text('请选择日期和时间区间'),
-          popupHeight: 780,
-          type: CalendarType.range,
-          initialValue: _dates,
-          onCellClick: (value, selectType, tdate) {
-            if (selectType == DateSelectType.start) {
-              panelKey.currentState?.switchTab(0);
-            } else if (selectType == DateSelectType.end) {
-              panelKey.currentState?.switchTab(1);
-            }
-          },
-          popupBottomBuilder: (_, __) => _RangeTimePickerPanel(
-            key: panelKey,
-            initialStartTime: popupStartTime,
-            initialEndTime: popupEndTime,
-            onChanged: (isStart, hour, min) {
-              if (isStart) {
-                popupStartTime = [hour, min];
-              } else {
-                popupEndTime = [hour, min];
-              }
-            },
-          ),
-          onConfirm: (value) {
-            if (value.length < 2) {
-              return;
-            }
-            final merged = [
-              value[0].copyWith(
-                hour: popupStartTime[0],
-                minute: popupStartTime[1],
-                second: 0,
-                millisecond: 0,
-              ),
-              value[1].copyWith(
-                hour: popupEndTime[0],
-                minute: popupEndTime[1],
-                second: 0,
-                millisecond: 0,
-              ),
-            ];
-            setState(() {
-              _dates = merged;
-              _startTime = popupStartTime;
-              _endTime = popupEndTime;
-            });
-          },
-        );
-      },
-    );
-  }
-}
-
-// ========================= 6. 锚点 =========================
+// ========================= 4. 锚点 =========================
 /// 锚点定位
 ///
 /// 演示 [TCalendar.showPopup] 的 anchorDate 参数,
@@ -427,19 +242,6 @@ class _AnchorCalendarCellState extends State<_AnchorCalendarCell> {
   }
 }
 
-// ===== 共用:构建时间选择器 items =====
-/// 构建时间选择器的列数据(24 小时 × 60 分钟)
-TPickerColumns _buildTimeItems() => TPickerColumns([
-      [
-        for (int i = 0; i < 24; i++)
-          TPickerOption(label: '${i.toString().padLeft(2, '0')}时', value: i),
-      ],
-      [
-        for (int i = 0; i < 60; i++)
-          TPickerOption(label: '${i.toString().padLeft(2, '0')}分', value: i),
-      ],
-    ]);
-
 // ===== 顶层格式化辅助函数 =====
 String _formatYmd(List dates) {
   if (dates.isEmpty) {
@@ -454,12 +256,6 @@ String _formatMd(DateTime d) {
   return '${d.month}/${d.day}';
 }
 
-String _formatMdHm(DateTime d) {
-  return '${d.month}/${d.day} '
-      '${d.hour.toString().padLeft(2, '0')}:'
-      '${d.minute.toString().padLeft(2, '0')}';
-}
-
 String _formatYmdFull(DateTime d) {
   return '${d.year}-${d.month.toString().padLeft(2, '0')}-'
       '${d.day.toString().padLeft(2, '0')}';
@@ -719,155 +515,6 @@ class _RangeSegment extends StatelessWidget {
   }
 }
 
-class _TimePickerPanel extends StatefulWidget {
-  const _TimePickerPanel({
-    required this.initialHour,
-    required this.initialMinute,
-    required this.title,
-    required this.onChange,
-  });
-
-  final int initialHour;
-  final int initialMinute;
-  final String title;
-  final void Function(int hour, int minute) onChange;
-
-  @override
-  State<_TimePickerPanel> createState() => _TimePickerPanelState();
-}
-
-class _TimePickerPanelState extends State<_TimePickerPanel> {
-  late final TPickerColumns _items;
-  late final List _initialValue;
-
-  @override
-  void initState() {
-    super.initState();
-    _items = _buildTimeItems();
-    _initialValue = [widget.initialHour, widget.initialMinute];
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      decoration: _bottomCardDecoration(context),
-      child: TPicker(
-        items: _items,
-        initialValue: _initialValue,
-        height: 180,
-        itemCount: 5,
-        title: widget.title,
-        onChange: (v) {
-          final values = v.values;
-          if (values.length >= 2 && values[0] is int && values[1] is int) {
-            widget.onChange(values[0] as int, values[1] as int);
-          }
-        },
-      ),
-    );
-  }
-}
-
-/// 区间+时间 demo 的双时间选择器面板(Tab 切换开始/结束)
-///
-/// 外部可通过 `switchTab` 方法驱动 Tab 切换(如日历 onCellClick 触发)。
-/// 每个 Tab 内部的时间选择值会被缓存,切回时恢复。
-class _RangeTimePickerPanel extends StatefulWidget {
-  const _RangeTimePickerPanel({
-    super.key,
-    required this.initialStartTime,
-    required this.initialEndTime,
-    required this.onChanged,
-  });
-
-  final List initialStartTime;
-  final List initialEndTime;
-  final void Function(bool isStart, int hour, int minute) onChanged;
-
-  @override
-  State<_RangeTimePickerPanel> createState() => _RangeTimePickerPanelState();
-}
-
-class _RangeTimePickerPanelState extends State<_RangeTimePickerPanel>
-    with SingleTickerProviderStateMixin {
-  late final TPickerColumns _items;
-  late final TabController _tabController;
-  int _tab = 0;
-  // 缓存用户在每个 tab 上最后选择的时分,不依赖 widget props 重置
-  late List _startSelected;
-  late List _endSelected;
-
-  @override
-  void initState() {
-    super.initState();
-    _items = _buildTimeItems();
-    _tabController = TabController(length: 2, vsync: this);
-    _startSelected = List.from(widget.initialStartTime);
-    _endSelected = List.from(widget.initialEndTime);
-  }
-
-  @override
-  void dispose() {
-    _tabController.dispose();
-    super.dispose();
-  }
-
-  List get _currentInitialValue =>
-      _tab == 0 ? _startSelected : _endSelected;
-
-  /// 由外部(日历 onCellClick)驱动切换 tab
-  void switchTab(int tab) {
-    if (_tab == tab) {
-      return;
-    }
-    setState(() => _tab = tab);
-    _tabController.animateTo(tab);
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      decoration: _bottomCardDecoration(context),
-      child: Column(
-        mainAxisSize: MainAxisSize.min,
-        children: [
-          TTabBar(
-            height: 40,
-            showIndicator: true,
-            controller: _tabController,
-            tabs: const [
-              TTab(text: '开始时间'),
-              TTab(text: '结束时间'),
-            ],
-            onTap: (i) => setState(() => _tab = i),
-          ),
-          TPicker(
-            key: ValueKey(_tab),
-            items: _items,
-            initialValue: _currentInitialValue,
-            height: 180,
-            itemCount: 5,
-            onChange: (v) {
-              final values = v.values;
-              if (values.length >= 2 && values[0] is int && values[1] is int) {
-                final h = values[0] as int;
-                final m = values[1] as int;
-                // 缓存当前 tab 的选中值,切回 tab 时恢复位置
-                if (_tab == 0) {
-                  _startSelected = [h, m];
-                } else {
-                  _endSelected = [h, m];
-                }
-                widget.onChanged(_tab == 0, h, m);
-              }
-            },
-          ),
-        ],
-      ),
-    );
-  }
-}
-
 /// 「组件样式 - 自定义文案 / 按钮 / 单元格」
 @Demo(group: 'calendar')
 Widget _buildStyle(BuildContext context) {
diff --git a/tdesign-component/lib/src/components/calendar/t_calendar.dart b/tdesign-component/lib/src/components/calendar/t_calendar.dart
index 2d53b4aff..31b7db7fe 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar.dart
@@ -38,12 +38,12 @@ class TCalendarInherited extends InheritedWidget {
     this.popupConfirmBtn,
     this.onConfirm,
     this.confirmBtnBuilder,
-    this.popupBottomBuilder,
-    this.popupBottomExpanded,
+    this.popupOverlayBuilder,
+    this.popupOverlayExpanded,
     Key? key,
   })  : assert(
-          popupBottomExpanded == null || popupBottomBuilder != null,
-          'popupBottomExpanded 需配合 popupBottomBuilder 使用',
+          popupOverlayExpanded == null || popupOverlayBuilder != null,
+          'popupOverlayExpanded 需配合 popupOverlayBuilder 使用',
         ),
         super(child: child, key: key);
 
@@ -78,18 +78,20 @@ class TCalendarInherited extends InheritedWidget {
   /// 自定义确认按钮;[onConfirm] 与默认确认按钮一致(回传选中值并关闭弹窗)。
   final Widget Function(VoidCallback onConfirm)? confirmBtnBuilder;
 
-  /// 弹窗底部自定义区域构建器(仅弹窗模式,由 [TCalendar.showPopup] 或手动
-  /// [TCalendarInherited] 注入)。
+  /// 弹窗模式下日历内容区底部浮层构建器(非 [TPopup] 面板底部)。
+  ///
+  /// 由 [TCalendar.showPopup] 或手动 [TCalendarInherited] 注入;
+  /// [selectedDates] 随点选实时更新。
   final Widget Function(BuildContext context, List selectedDates)?
-      popupBottomBuilder;
+      popupOverlayBuilder;
 
-  /// 弹窗底部区域是否展开(响应式),需配合 [popupBottomBuilder]。
-  final ValueListenable? popupBottomExpanded;
+  /// 浮层是否展开(响应式),需配合 [popupOverlayBuilder]。
+  final ValueListenable? popupOverlayExpanded;
 
   /// 仅当 Inherited 上的**静态配置**变化时通知依赖方重建。
   ///
   /// [selected] 为 [ValueNotifier],变更走 [selectedListenable],不依赖本方法。
-  /// 若返回 `false`,在运行期替换 [popupBottomBuilder] 等回调时,子树不会自动重建,
+  /// 若返回 `false`,在运行期替换 [popupOverlayBuilder] 等回调时,子树不会自动重建,
   /// 弹窗场景一般在 push 时一次性注入,内嵌高级用法请整体替换 Inherited。
   @override
   bool updateShouldNotify(covariant TCalendarInherited oldWidget) {
@@ -99,8 +101,8 @@ class TCalendarInherited extends InheritedWidget {
         oldWidget.onClose != onClose ||
         oldWidget.onConfirm != onConfirm ||
         oldWidget.confirmBtnBuilder != confirmBtnBuilder ||
-        oldWidget.popupBottomBuilder != popupBottomBuilder ||
-        oldWidget.popupBottomExpanded != popupBottomExpanded;
+        oldWidget.popupOverlayBuilder != popupOverlayBuilder ||
+        oldWidget.popupOverlayExpanded != popupOverlayExpanded;
   }
 
   static TCalendarInherited? of(BuildContext context) {
@@ -263,7 +265,7 @@ class TCalendar extends StatefulWidget {
   /// 弹出日历选择器,返回选中的日期列表。
   ///
   /// 取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。
-  /// 弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`,
+  /// 弹窗内点选过程无 [onChange];实时联动请用 [popupOverlayBuilder] 的 `dates`,
   /// 或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。
   ///
   /// ```dart
@@ -281,7 +283,7 @@ class TCalendar extends StatefulWidget {
   /// / [TPopupOptions.bottom] 自行组装。
   static Future?> showPopup(
     BuildContext context, {
-    /// 弹窗标题组件
+    /// 弹窗标题组件,由 [TPopupOptions.bottom] 头部承载(不传入内层 [TCalendar])。
     Widget? titleWidget,
 
     /// 日历选择模式
@@ -314,12 +316,12 @@ class TCalendar extends StatefulWidget {
     /// 自定义样式
     TCalendarStyle? style,
 
-    /// 弹窗底部自定义区域构建器(经 [TCalendarInherited] 注入,仅弹窗内生效)。
+    /// 弹窗模式下日历内容区底部浮层(经 [TCalendarInherited] 注入,仅弹窗内生效)。
     Widget Function(BuildContext context, List selectedDates)?
-        popupBottomBuilder,
+        popupOverlayBuilder,
 
-    /// 弹窗底部区域是否展开(响应式),需配合 [popupBottomBuilder]。
-    ValueListenable? popupBottomExpanded,
+    /// 浮层是否展开(响应式),需配合 [popupOverlayBuilder]。
+    ValueListenable? popupOverlayExpanded,
 
     /// 自定义确认按钮,[onConfirm] 与默认确认按钮一致。
     Widget Function(VoidCallback onConfirm)? confirmBtnBuilder,
@@ -337,9 +339,6 @@ class TCalendar extends StatefulWidget {
       TCalendarCellModel cell,
     )? onCellClick,
 
-    /// 点击遮罩或物理返回是否关闭
-    bool autoClose = true,
-
     TCalendarCellBuilder? cellBuilder,
     TCalendarSubtitleBuilder? subtitleBuilder,
     TCalendarDataSource? dataSource,
@@ -379,61 +378,48 @@ class TCalendar extends StatefulWidget {
         _calcDefaultHeight(mediaQuery.padding.bottom, mediaQuery.size.height);
     final calendarHeight =
         (panelHeight - _kPanelHeaderHeight).clamp(0.0, double.infinity);
+    final resolvedStyle = style ?? TCalendarStyle.generateStyle(context);
+    final popupTitleWidget = wrapCalendarTitleWidget(
+      titleWidget,
+      titleStyle: resolvedStyle.titleStyle,
+      titleMaxLine: resolvedStyle.titleMaxLine,
+    );
 
     handle = TPopup.show(
       context,
       options: TPopupOptions.bottom(
-        headerBuilder: (ctx, close) => Padding(
-          padding: const EdgeInsets.symmetric(horizontal: 16),
-          child: Stack(
-            alignment: Alignment.center,
-            children: [
-              if (titleWidget != null) Center(child: titleWidget),
-              if (autoClose)
-                Positioned(
-                  right: -8,
-                  child: IconButton(
-                    icon: Icon(
-                      TIcons.close,
-                      color: style?.titleCloseColor,
-                    ),
-                    onPressed: close,
-                  ),
-                ),
-            ],
+        titleWidget: popupTitleWidget,
+        cancelBuilder: null,
+        confirmBuilder: (ctx, close) => IconButton(
+          icon: Icon(
+            TIcons.close,
+            color: style?.titleCloseColor,
           ),
+          onPressed: close,
         ),
-        titleWidget: null,
-        cancelBuilder: null,
-        confirmBuilder: null,
         height: panelHeight,
-        closeOnOverlayClick: autoClose,
+        closeOnOverlayClick: true,
         onClosed: completeClose,
         child: PopScope(
-          canPop: autoClose,
+          canPop: true,
           child: TCalendarInherited(
             selected: selected,
             usePopup: true,
             popupControls: false,
             popupConfirmBtn: true,
             confirmBtnBuilder: confirmBtnBuilder,
-            popupBottomBuilder: popupBottomBuilder,
-            popupBottomExpanded: popupBottomExpanded,
+            popupOverlayBuilder: popupOverlayBuilder,
+            popupOverlayExpanded: popupOverlayExpanded,
             onClose: () {
-              if (autoClose) {
-                closePopup();
-              }
+              closePopup();
             },
             onConfirm: () {
               result = List.from(selected.value);
               onConfirm?.call(result!);
-              if (autoClose) {
-                closePopup();
-              }
+              closePopup();
             },
             child: TCalendar(
               height: calendarHeight,
-              titleWidget: titleWidget,
               type: type,
               initialValue: initialValue,
               minDate: minDate,
@@ -562,10 +548,10 @@ class _TCalendarState extends State {
   @override
   Widget build(BuildContext context) {
     final verticalGap = _style.verticalGap ?? TTheme.of(context).spacer8;
-    final popupBottomBuilder = inherited?.popupBottomBuilder;
-    final popupBottomExpanded = inherited?.popupBottomExpanded;
+    final popupOverlayBuilder = inherited?.popupOverlayBuilder;
+    final popupOverlayExpanded = inherited?.popupOverlayExpanded;
     final hasBottom =
-        inherited?.usePopup == true && popupBottomBuilder != null;
+        inherited?.usePopup == true && popupOverlayBuilder != null;
 
     Widget stackContent(bool bottomExpanded) {
       return Stack(
@@ -577,9 +563,9 @@ class _TCalendarState extends State {
       );
     }
 
-    final child = hasBottom && popupBottomExpanded != null
+    final child = hasBottom && popupOverlayExpanded != null
         ? ValueListenableBuilder(
-            valueListenable: popupBottomExpanded,
+            valueListenable: popupOverlayExpanded,
             builder: (context, expanded, _) => stackContent(expanded),
           )
         : stackContent(hasBottom);
@@ -662,7 +648,7 @@ class _TCalendarState extends State {
     if (!hasBottom) {
       return body;
     }
-    if (inherited?.popupBottomExpanded == null) {
+    if (inherited?.popupOverlayExpanded == null) {
       return Padding(
         padding: const EdgeInsets.only(bottom: _bottomPeekHeight),
         child: body,
@@ -686,7 +672,7 @@ class _TCalendarState extends State {
       anchorDate: widget.anchorDate,
       anchorRevision: widget.anchorRevision,
       minDate: widget.minDate,
-      value: _cachedValueDates,
+      initialValue: _cachedValueDates,
       bodyPadding: _style.bodyPadding ?? TTheme.of(context).spacer16,
       monthNames: monthNames,
       monthTitleStyle: _style.monthTitleStyle,
@@ -723,7 +709,7 @@ class _TCalendarState extends State {
   ///
   /// single:每月最多一个 selected,遇到即覆盖 _selectedSingleRef。
   /// multiple:把当月所有 selected 的引用按 date 写入 map。
-  /// range:本身走 widget.value 重建路径,不需要登记。
+  /// range:本身走 initialValue 重建路径,不需要登记。
   void _handleCellGenerated(DateTime monthDate, List cells) {
     if (widget.type == CalendarType.range) {
       return;
@@ -835,22 +821,22 @@ class _TCalendarState extends State {
     return [tapped];
   }
 
-  // 行为约定详见 [TCalendarInherited.popupBottomBuilder]。
+  // 行为约定详见 [TCalendarInherited.popupOverlayBuilder]。
   Widget _buildBottom(bool bottomExpanded) {
-    final popupBottomBuilder = inherited!.popupBottomBuilder!;
+    final popupOverlayBuilder = inherited!.popupOverlayBuilder!;
     final bottomOffset = _calcBottomOffset();
 
     final content = ValueListenableBuilder>(
       valueListenable: inherited!.selected,
       builder: (context, selectedDates, _) {
-        return popupBottomBuilder(
+        return popupOverlayBuilder(
           context,
           List.unmodifiable(selectedDates),
         );
       },
     );
 
-    if (inherited!.popupBottomExpanded != null) {
+    if (inherited!.popupOverlayExpanded != null) {
       return Positioned(
         left: 0,
         right: 0,
@@ -885,7 +871,7 @@ class _TCalendarState extends State {
       final btnPadding = widget.safeAreaInset
           ? TTheme.of(context).spacer16
           : TTheme.of(context).spacer16 * 2;
-      // 默认与自定义确认按钮均预留固定高度,避免 popupBottomBuilder 浮层重叠。
+      // 默认与自定义确认按钮均预留固定高度,避免 popupOverlayBuilder 浮层重叠。
       // 若自定义按钮更高,请在 popupHeight 中额外预留空间。
       return safeBottom + btnPadding + _confirmBtnHeight;
     }
diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
index 904946c80..03fedb5ef 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar_body.dart
@@ -10,7 +10,7 @@ class TCalendarBody extends StatefulWidget {
     this.maxDate,
     this.minDate,
     required this.type,
-    this.value,
+    this.initialValue,
     required this.firstDayOfWeek,
     required this.builder,
     required this.bodyPadding,
@@ -31,7 +31,7 @@ class TCalendarBody extends StatefulWidget {
   final DateTime? maxDate;
   final DateTime? minDate;
   final CalendarType type;
-  final List? value;
+  final List? initialValue;
   final DateTime? anchorDate;
   final int firstDayOfWeek;
   final Widget Function(
@@ -113,9 +113,10 @@ class _TCalendarBodyState extends State {
       widget.onCacheInvalidated?.call();
       _lastNotifiedMonthKey = null;
       _initMonths();
-    } else if (!_listEqualsDate(oldWidget.value, widget.value)) {
-      // 选中值由上层更新(initialValue / _cachedValueDates)时清空月份缓存,
-      // 让所有 cell 基于新 value 重建 typeNotifier,避免 single/multiple/range 残留旧态。
+    } else if (!_listEqualsDate(
+        oldWidget.initialValue, widget.initialValue)) {
+      // 选中值由上层更新(TCalendar.initialValue / _cachedValueDates)时清空月份缓存,
+      // 让所有 cell 基于新 initialValue 重建 typeNotifier,避免 single/multiple/range 残留旧态。
       _data.clear();
       widget.onCacheInvalidated?.call();
     }
@@ -220,10 +221,10 @@ class _TCalendarBodyState extends State {
   double _calcScrollOffset() {
     var scrollToDate = widget.anchorDate;
     if (scrollToDate == null) {
-      if (widget.value == null || widget.value!.isEmpty) {
+      if (widget.initialValue == null || widget.initialValue!.isEmpty) {
         return 0.0;
       }
-      scrollToDate = widget.value!.reduce((a, b) => a.isBefore(b) ? a : b);
+      scrollToDate = widget.initialValue!.reduce((a, b) => a.isBefore(b) ? a : b);
     }
     if (_months.isEmpty) {
       return 0.0;
@@ -513,27 +514,27 @@ class _TCalendarBodyState extends State {
       if (date.compareTo(min) == -1 || date.compareTo(max) == 1) {
         selectType = DateSelectType.disabled;
       } else if (widget.type == CalendarType.single &&
-          (widget.value?.length ?? 0) >= 1) {
-        if (date.compareTo(widget.value![0]) == 0) {
+          (widget.initialValue?.length ?? 0) >= 1) {
+        if (date.compareTo(widget.initialValue![0]) == 0) {
           selectType = DateSelectType.selected;
         }
       } else if (widget.type == CalendarType.multiple &&
-          widget.value != null) {
-        if (widget.value!.isContains((e) => date.compareTo(e) == 0)) {
+          widget.initialValue != null) {
+        if (widget.initialValue!.isContains((e) => date.compareTo(e) == 0)) {
           selectType = DateSelectType.selected;
         }
       } else if (widget.type == CalendarType.range &&
-          (widget.value?.length ?? 0) >= 1) {
+          (widget.initialValue?.length ?? 0) >= 1) {
         final end =
-            (widget.value?.length ?? 0) > 1 ? widget.value![1] : null;
-        if (date.compareTo(widget.value![0]) == 0) {
+            (widget.initialValue?.length ?? 0) > 1 ? widget.initialValue![1] : null;
+        if (date.compareTo(widget.initialValue![0]) == 0) {
           selectType = DateSelectType.start;
         }
-        if (end != null && widget.value![0].compareTo(end) < 0) {
+        if (end != null && widget.initialValue![0].compareTo(end) < 0) {
           if (date.compareTo(end) == 0) {
             selectType = DateSelectType.end;
           }
-          if (date.compareTo(widget.value![0]) == 1 &&
+          if (date.compareTo(widget.initialValue![0]) == 1 &&
               date.compareTo(end) == -1) {
             selectType = DateSelectType.centre;
           }
diff --git a/tdesign-component/lib/src/components/calendar/t_calendar_header.dart b/tdesign-component/lib/src/components/calendar/t_calendar_header.dart
index d6105568d..d3cfde6ad 100644
--- a/tdesign-component/lib/src/components/calendar/t_calendar_header.dart
+++ b/tdesign-component/lib/src/components/calendar/t_calendar_header.dart
@@ -2,6 +2,28 @@ import 'package:flutter/material.dart';
 import '../../../tdesign_flutter.dart';
 import 't_calendar_cell.dart';
 
+/// 为 [TCalendar.titleWidget] 应用 [TCalendarStyle.titleStyle] / [titleMaxLine]。
+Widget? wrapCalendarTitleWidget(
+  Widget? titleWidget, {
+  TextStyle? titleStyle,
+  int? titleMaxLine,
+  TextOverflow titleOverflow = TextOverflow.ellipsis,
+}) {
+  if (titleWidget == null) {
+    return null;
+  }
+  if (titleStyle == null && titleMaxLine == null) {
+    return titleWidget;
+  }
+  return DefaultTextStyle.merge(
+    style: titleStyle,
+    maxLines: titleMaxLine,
+    overflow: titleOverflow,
+    textAlign: TextAlign.center,
+    child: titleWidget,
+  );
+}
+
 class TCalendarHeader extends StatelessWidget {
   const TCalendarHeader({
     Key? key,
@@ -59,7 +81,14 @@ class TCalendarHeader extends StatelessWidget {
                   if (closeBtn) const SizedBox(width: 24),
                   Expanded(
                     child: Center(
-                      child: titleWidget ?? const SizedBox.shrink(),
+                      child: wrapCalendarTitleWidget(
+                            titleWidget,
+                            titleStyle: titleStyle,
+                            titleMaxLine: titleMaxLine,
+                            titleOverflow:
+                                titleOverflow ?? TextOverflow.ellipsis,
+                          ) ??
+                          const SizedBox.shrink(),
                     ),
                   ),
                   if (closeBtn)
diff --git a/tdesign-component/test/t_calendar_test.dart b/tdesign-component/test/t_calendar_test.dart
index 7caf68ea6..d763d579c 100644
--- a/tdesign-component/test/t_calendar_test.dart
+++ b/tdesign-component/test/t_calendar_test.dart
@@ -16,21 +16,21 @@ DateTime _day(int y, int m, int d) => DateTime(y, m, d);
 
 void main() {
   // -----------------------------------------------------------------------
-  // popupBottomBuilder / popupBottomExpanded(仅 TCalendarInherited / showPopup)
+  // popupOverlayBuilder / popupOverlayExpanded(仅 TCalendarInherited / showPopup)
   // -----------------------------------------------------------------------
-  group('TCalendar — popupBottom / popupBottomExpanded', () {
-    test('popupBottomExpanded 未配合 popupBottomBuilder 时触发 assert', () {
+  group('TCalendar — popupOverlay / popupOverlayExpanded', () {
+    test('popupOverlayExpanded 未配合 popupOverlayBuilder 时触发 assert', () {
       expect(
         () => TCalendarInherited(
           selected: ValueNotifier>([]),
-          popupBottomExpanded: ValueNotifier(false),
+          popupOverlayExpanded: ValueNotifier(false),
           child: const SizedBox(),
         ),
         throwsAssertionError,
       );
     });
 
-    testWidgets('内嵌模式无法通过 TCalendar 传入 popupBottomBuilder', (tester) async {
+    testWidgets('内嵌模式无法通过 TCalendar 传入 popupOverlayBuilder', (tester) async {
       await tester.pumpWidget(
         _buildTestApp(
           TCalendar(
@@ -43,13 +43,13 @@ void main() {
       expect(find.text('底部'), findsNothing);
     });
 
-    testWidgets('非弹窗 Inherited 不渲染 popupBottomBuilder', (tester) async {
+    testWidgets('非弹窗 Inherited 不渲染 popupOverlayBuilder', (tester) async {
       await tester.pumpWidget(
         _buildTestApp(
           TCalendarInherited(
             selected: ValueNotifier>([]),
             usePopup: false,
-            popupBottomBuilder: (_, __) => const Text('底部'),
+            popupOverlayBuilder: (_, __) => const Text('底部'),
             child: TCalendar(
               titleWidget: const Text('测试'),
               height: 640,
@@ -61,7 +61,7 @@ void main() {
       expect(find.text('底部'), findsNothing);
     });
 
-    testWidgets('popup 内选中变化时 popupBottomBuilder 会重建', (tester) async {
+    testWidgets('popup 内选中变化时 popupOverlayBuilder 会重建', (tester) async {
       final day = _day(2024, 6, 15);
       final selected = ValueNotifier>([day]);
 
@@ -70,7 +70,7 @@ void main() {
           TCalendarInherited(
             selected: selected,
             usePopup: true,
-            popupBottomBuilder: (_, dates) => Text('days:${dates.length}'),
+            popupOverlayBuilder: (_, dates) => Text('days:${dates.length}'),
             child: TCalendar(
               titleWidget: const Text('测试'),
               height: 640,
@@ -89,7 +89,7 @@ void main() {
       expect(find.text('days:2'), findsOneWidget);
     });
 
-    testWidgets('popup 内 popupBottomExpanded 为 false 时处于收起偏移', (tester) async {
+    testWidgets('popup 内 popupOverlayExpanded 为 false 时处于收起偏移', (tester) async {
       final expanded = ValueNotifier(false);
       final selected = ValueNotifier>([]);
 
@@ -98,8 +98,8 @@ void main() {
           TCalendarInherited(
             selected: selected,
             usePopup: true,
-            popupBottomExpanded: expanded,
-            popupBottomBuilder: (_, __) => const Text('底部内容'),
+            popupOverlayExpanded: expanded,
+            popupOverlayBuilder: (_, __) => const Text('底部内容'),
             child: TCalendar(
               titleWidget: const Text('测试'),
               height: 640,
@@ -654,7 +654,7 @@ void main() {
       expect(popupResult!.single, day20);
     });
 
-    testWidgets('popupBottomBuilder 与确认按钮区域不重叠', (tester) async {
+    testWidgets('popupOverlayBuilder 与确认按钮区域不重叠', (tester) async {
       await tester.pumpWidget(
         _buildTestApp(
           Builder(
@@ -667,7 +667,7 @@ void main() {
                   minDate: _day(2024, 6, 1),
                   maxDate: _day(2024, 6, 30),
                   popupHeight: 640,
-                  popupBottomBuilder: (_, __) => const Text('底部区域'),
+                  popupOverlayBuilder: (_, __) => const Text('底部区域'),
                 );
               },
               child: const Text('open'),
diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md
index ca0815153..99a89f496 100644
--- a/tdesign-site/src/calendar/README.md
+++ b/tdesign-site/src/calendar/README.md
@@ -465,7 +465,7 @@ Widget _buildLunar(BuildContext context) {
 
 | 名称 | 返回类型 | 参数 | 说明 |
 | --- | --- | --- | --- |
-| showPopup |  |   required BuildContext context,  Widget? titleWidget,  CalendarType type,  List? initialValue,  DateTime? minDate,  DateTime? maxDate,  DateTime? anchorDate,  int anchorRevision,  double? popupHeight,  int firstDayOfWeek,  double? cellHeight,  TCalendarStyle? style,  Widget Function(BuildContext context, List selectedDates)? popupBottomBuilder,  ValueListenable? popupBottomExpanded,  Widget Function(VoidCallback onConfirm)? confirmBtnBuilder,  void Function(List)? onConfirm,  VoidCallback? onClose,  void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick,  bool autoClose,  TCalendarCellBuilder? cellBuilder,  TCalendarSubtitleBuilder? subtitleBuilder,  TCalendarDataSource? dataSource,  ValueChanged? onMonthChange,  Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。     取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。   弹窗内点选过程无 [onChange];实时联动请用 [popupBottomBuilder] 的 `dates`,   或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。     ```dart   final result = await TCalendar.showPopup(     context,     titleWidget: Text('请选择日期'),     type: CalendarType.single,   );   if (result != null) {     print('选中了: $result');   }   ```     若需完全自定义布局,请直接使用 [TCalendar] + [TPopup.show] / [TPopupOptions.bottom] 自行组装。 |
+| showPopup |  |   required BuildContext context,  Widget? titleWidget,  CalendarType type,  List? initialValue,  DateTime? minDate,  DateTime? maxDate,  DateTime? anchorDate,  int anchorRevision,  double? popupHeight,  int firstDayOfWeek,  double? cellHeight,  TCalendarStyle? style,  Widget Function(BuildContext context, List selectedDates)? popupOverlayBuilder,  ValueListenable? popupOverlayExpanded,  Widget Function(VoidCallback onConfirm)? confirmBtnBuilder,  void Function(List)? onConfirm,  VoidCallback? onClose,  void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick,  TCalendarCellBuilder? cellBuilder,  TCalendarSubtitleBuilder? subtitleBuilder,  TCalendarDataSource? dataSource,  ValueChanged? onMonthChange,  Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。     取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。   弹窗内点选过程无 [onChange];实时联动请用 [popupOverlayBuilder] 的 `dates`,   或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。     ```dart   final result = await TCalendar.showPopup(     context,     titleWidget: Text('请选择日期'),     type: CalendarType.single,   );   if (result != null) {     print('选中了: $result');   }   ```     若需完全自定义布局,请直接使用 [TCalendar] + [TPopup.show] / [TPopupOptions.bottom] 自行组装。 |
 
 ```
 ```
@@ -480,8 +480,8 @@ Widget _buildLunar(BuildContext context) {
 | key |  | - |  |
 | onClose |  | - |  |
 | onConfirm |  | - |  |
-| popupBottomBuilder | Widget Function(BuildContext context, List selectedDates)? | - | 弹窗底部自定义区域构建器(仅弹窗模式,由 [TCalendar.showPopup] 或手动 |
-| popupBottomExpanded | ValueListenable? | - | 弹窗底部区域是否展开(响应式),需配合 [popupBottomBuilder]。 |
+| popupOverlayBuilder | Widget Function(BuildContext context, List selectedDates)? | - | 弹窗模式下日历内容区底部浮层构建器(非 TPopup 面板底部),由 [TCalendar.showPopup] 或手动 |
+| popupOverlayExpanded | ValueListenable? | - | 浮层是否展开(响应式),需配合 [popupOverlayBuilder]。 |
 | popupConfirmBtn | bool? | - | 是否由 [TCalendar] 渲染底部确认按钮。 |
 | popupControls | bool | true | 是否由 [TCalendar] 自行渲染关闭按钮和标题行。 |
 | selected | ValueNotifier> | - | 选中态的可写引用(仅供 [TCalendar] 内部更新使用)。 |

From 6adbed0fb7b9d4d7720f1ad1595ca1a37533ddf1 Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Thu, 28 May 2026 08:07:22 +0000
Subject: [PATCH 35/35] [autofix.ci] apply automated fixes

---
 .../example/assets/api/calendar_api.md        | 224 ++++++-----
 tdesign-site/src/action-sheet/README.md       |  70 ++--
 tdesign-site/src/button/README.md             |  26 +-
 tdesign-site/src/calendar/README.md           | 224 ++++++++---
 tdesign-site/src/cell/README.md               |  22 +-
 tdesign-site/src/collapse/README.md           |  36 +-
 tdesign-site/src/dialog/README.md             |  81 ++--
 tdesign-site/src/drawer/README.md             |   6 +-
 tdesign-site/src/dropdown-menu/README.md      |  10 +-
 tdesign-site/src/image/README.md              |   2 +-
 tdesign-site/src/message/README.md            |  42 ++-
 tdesign-site/src/notice-bar/README.md         |  24 +-
 tdesign-site/src/picker/README.md             | 132 +++++--
 tdesign-site/src/popup/README.md              | 357 ++++++------------
 tdesign-site/src/pull-down-refresh/README.md  |   6 +-
 tdesign-site/src/rate/README.md               |  14 +-
 tdesign-site/src/swipe-cell/README.md         |  47 ++-
 tdesign-site/src/swiper/README.md             |  16 +-
 tdesign-site/src/tag/README.md                |  46 ++-
 tdesign-site/src/text/README.md               |  97 +++--
 tdesign-site/src/time-counter/README.md       |  28 +-
 21 files changed, 843 insertions(+), 667 deletions(-)

diff --git a/tdesign-component/example/assets/api/calendar_api.md b/tdesign-component/example/assets/api/calendar_api.md
index ebe8e8487..b71884071 100644
--- a/tdesign-component/example/assets/api/calendar_api.md
+++ b/tdesign-component/example/assets/api/calendar_api.md
@@ -2,61 +2,115 @@
 ### TCalendar
 #### 简介
 日历组件
+
+#### 静态方法
+
+##### TCalendar.showPopup
+
+弹出日历选择器,返回选中的日期列表。
+取消或关闭弹窗时返回 `null`;点击确认时返回选中的 `DateTime` 列表。
+弹窗内点选过程无 `onChange`;实时联动请用 `popupOverlayBuilder` 的 `dates`,
+或自行用 `TCalendarInherited` 监听 `TCalendarInherited.selectedListenable`。
+```dart
+final result = await TCalendar.showPopup(
+  context,
+  titleWidget: Text('请选择日期'),
+  type: CalendarType.single,
+);
+if (result != null) {
+  print('选中了: $result');
+}
+```
+若需完全自定义布局,请直接使用 `TCalendar` + `TPopup.show`
+/ `TPopupOptions.bottom` 自行组装。
+
+返回类型:`Future?>`
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | - |
+| titleWidget | Widget? | - | 标题组件,可传入 Text 或自定义 Widget |
+| type | CalendarType | CalendarType.single | 日历的选择模式,决定点击日期后的选中行为: - `CalendarType.single`:单选,点击新日期取消旧选中 - `CalendarType.multiple`:多选,点击切换选中/取消 - `CalendarType.range`:区间选择,依次选起止日期 |
+| initialValue | List? | - | 初始选中日期列表,不传则默认今天。 **非受控语义**:仅用于首次挂载;用户点选后以 `onChange` 为准,由调用方自行 `setState` 保存。若父组件在运行期修改本参数,会同步选中态并刷新格子(与 range 行为一致)。 列表长度与 `type` 对应: - `CalendarType.single`:1 个元素(选中日期) - `CalendarType.multiple`:N 个元素(所有选中日期) - `CalendarType.range`:2 个元素(起始、结束日期) |
+| minDate | DateTime? | - | 最小可选的日期,不传则默认 1970-01-01 |
+| maxDate | DateTime? | - | 最大可选的日期,不传则默认 2100-12-31 |
+| anchorDate | DateTime? | - | 锚点日期,打开时滚动到该日期所在月份。 |
+| anchorRevision | int | 0 | 锚点滚动触发序号,默认 `0`。 与 `anchorDate` 配合:序号递增可重复滚到同一月份;仅改月份时也可只更新 `anchorDate`。 |
+| popupHeight | double? | - | - |
+| firstDayOfWeek | int | 0 | 第一天从星期几开始,0 = 周日,1 = 周一,…,6 = 周六。默认 0(周日)。 |
+| cellHeight | double? | - | 日期单元格高度,默认 60。如需更大行高可传入自定义值(如 80) |
+| style | TCalendarStyle? | - | 自定义样式 |
+| popupOverlayBuilder | Widget Function(BuildContext context, List selectedDates)? | - | - |
+| popupOverlayExpanded | ValueListenable? | - | - |
+| confirmBtnBuilder | Widget Function(VoidCallback onConfirm)? | - | - |
+| onConfirm | void Function(List)? | - | - |
+| onClose | VoidCallback? | - | - |
+| onCellClick | void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? | - | 点击日期时触发 |
+| cellBuilder | TCalendarCellBuilder? | - | 整格自定义;设置后不再使用默认主区/副标题布局。 |
+| subtitleBuilder | TCalendarSubtitleBuilder? | - | 副标题完全自定义;未设置时可使用 `dataSource.getSubtitle`。 |
+| dataSource | TCalendarDataSource? | - | 可选数据源,提供副标题字符串(无 `subtitleBuilder` 时生效)。 |
+| onMonthChange | ValueChanged? | - | 月份变化时触发 |
+| monthTitleBuilder | Widget Function(BuildContext context, DateTime monthDate)? | - | 月标题构建器 |
+
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| anchorDate | DateTime? | - | 锚点日期 |
-| animateTo | bool? | false | 动画滚动到指定位置 |
-| cellHeight | double? | 60 | 日期高度 |
-| cellWidget | Widget? Function(BuildContext context, TDate tdate, DateSelectType selectType)? | - | 自定义日期单元格组件 |
-| dataSource | TCalendarDataSource? | - | 外部数据源,用于提供农历转换等功能 |
-| dateType | TCalendarDateType | TCalendarDateType.solar | 日历类型:阳历或农历 |
-| displayFormat | String? | 'year month' | 年月显示格式,`year`表示年,`month`表示月,如`year month`表示年在前、月在后、中间隔一个空格 |
-| firstDayOfWeek | int? | 0 | 第一天从星期几开始,默认 0 = 周日 |
-| format | CalendarFormat? | - | 用于格式化日期的函数,可定义日期前后的显示内容和日期样式 |
-| height | double? | - | 高度 |
-| isTimeUnit | bool? | true | 是否显示时间单位 |
+| anchorDate | DateTime? | - | 锚点日期,打开时滚动到该日期所在月份。 |
+| anchorRevision | int | 0 | 锚点滚动触发序号,默认 `0`。 与 `anchorDate` 配合:序号递增可重复滚到同一月份;仅改月份时也可只更新 `anchorDate`。 |
+| animateTo | bool | false | 滚动到选中日期/锚点日期所在月份时是否使用动画,默认 false |
+| cellBuilder | TCalendarCellBuilder? | - | 整格自定义;设置后不再使用默认主区/副标题布局。 |
+| cellHeight | double? | - | 日期单元格高度,默认 60。如需更大行高可传入自定义值(如 80) |
+| dataSource | TCalendarDataSource? | - | 可选数据源,提供副标题字符串(无 `subtitleBuilder` 时生效)。 |
+| firstDayOfWeek | int | 0 | 第一天从星期几开始,0 = 周日,1 = 周一,…,6 = 周六。默认 0(周日)。 |
+| height | double? | - | 高度,不传时内嵌模式自动按 5 行日期计算 |
+| initialValue | List? | - | 初始选中日期列表,不传则默认今天。 **非受控语义**:仅用于首次挂载;用户点选后以 `onChange` 为准,由调用方自行 `setState` 保存。若父组件在运行期修改本参数,会同步选中态并刷新格子(与 range 行为一致)。 列表长度与 `type` 对应: - `CalendarType.single`:1 个元素(选中日期) - `CalendarType.multiple`:N 个元素(所有选中日期) - `CalendarType.range`:2 个元素(起始、结束日期) |
 | key | Key? | - | 组件标识,用于区分或保留组件状态。 |
-| maxDate | int? | - | 最大可选的日期(fromMillisecondsSinceEpoch),不传则默认半年后 |
-| minDate | int? | - | 最小可选的日期(fromMillisecondsSinceEpoch),不传则默认今天 |
+| maxDate | DateTime? | - | 最大可选的日期,不传则默认 2100-12-31 |
+| minDate | DateTime? | - | 最小可选的日期,不传则默认 1970-01-01 |
 | monthTitleBuilder | Widget Function(BuildContext context, DateTime monthDate)? | - | 月标题构建器 |
-| monthTitleHeight | double? | 22 | 月标题高度 |
-| onCellClick | void Function(int value, DateSelectType type, TDate tdate)? | - | 点击日期时触发 |
-| onCellLongPress | void Function(int value, DateSelectType type, TDate tdate)? | - | 长安日期时触发 |
-| onChange | void Function(List value)? | - | 选中值变化时触发 |
-| onHeaderClick | void Function(int index, String week)? | - | 点击周时触发 |
+| monthTitleHeight | double | 22 | 每月标题行高度(如 '2025年6月' 所在行),默认 22 |
+| onCellClick | void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? | - | 点击日期时触发 |
+| onChange | void Function(List value)? | - | 选中值变化时触发 |
 | onMonthChange | ValueChanged? | - | 月份变化时触发 |
-| pickerHeight | double? | 178 | 时间选择器List的视窗高度 |
-| pickerItemCount | int? | 3 | 选择器List视窗中item个数,pickerHeight / pickerItemCount即item高度 |
-| showLunarInfo | bool | false | 阳历模式下是否显示农历信息作为副标题 |
+| safeAreaInset | bool | true | 是否适配底部安全区域(如 iPhone Home Indicator),默认 true |
 | style | TCalendarStyle? | - | 自定义样式 |
-| timePickerModel | List? | - | 自定义时间选择器 |
-| title | String? | - | 标题 |
-| titleWidget | Widget? | - | 标题组件 |
-| type | CalendarType? | CalendarType.single | 日历的选择类型,single = 单选;multiple = 多选;range = 区间选择 |
-| useSafeArea | bool? | true | 是否使用安全区域,默认true |
-| useTimePicker | bool? | false | 是否显示时间选择器 |
-| value | List? | - | 当前选择的日期(fromMillisecondsSinceEpoch),不传则默认今天,当 type = single 时数组长度为1 |
-| width | double? | - | 宽度 |
+| subtitleBuilder | TCalendarSubtitleBuilder? | - | 副标题完全自定义;未设置时可使用 `dataSource.getSubtitle`。 |
+| titleWidget | Widget? | - | 标题组件,可传入 Text 或自定义 Widget |
+| type | CalendarType | CalendarType.single | 日历的选择模式,决定点击日期后的选中行为: - `CalendarType.single`:单选,点击新日期取消旧选中 - `CalendarType.multiple`:多选,点击切换选中/取消 - `CalendarType.range`:区间选择,依次选起止日期 |
 
 
-### TCalendarPopup
+### TCalendarInherited
 #### 简介
-单元格组件popup模式
+日历弹窗状态的 InheritedWidget 容器。
+由上层(如 `TSlidePopupRoute` 的 builder)包裹在 `TCalendar` 外侧,
+将选中态、确认/关闭回调等注入子树。
+
+#### 静态方法
+
+##### TCalendarInherited.of
+
+返回类型:`TCalendarInherited?`
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | - |
+
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| context | BuildContext | - | 上下文 |
-| autoClose | bool? | true | 自动关闭;在点击关闭按钮、确认按钮、遮罩层时自动关闭 |
-| builder | CalendarBuilder? | - | 控件构建器,优先级高于`child` |
-| child | TCalendar? | - | 日历控件 |
-| confirmBtn | Widget? | - | 自定义确认按钮 |
-| onClose | VoidCallback? | - | 关闭时触发 |
-| onConfirm | void Function(List value)? | - | 点击确认按钮时触发 |
-| top | double? | - | 距离顶部的距离 |
-| visible | bool? | - | 默认是否显示日历 |
+| child | Widget | - | - |
+| confirmBtnBuilder | Widget Function(VoidCallback onConfirm)? | - | 自定义确认按钮;`onConfirm` 与默认确认按钮一致(回传选中值并关闭弹窗)。 |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
+| onClose | VoidCallback? | - | - |
+| onConfirm | VoidCallback? | - | - |
+| popupConfirmBtn | bool? | - | 是否由 `TCalendar` 渲染底部确认按钮。 为 `null`(默认)时跟随 `popupControls`;显式设置时覆盖。 |
+| popupControls | bool | true | 是否由 `TCalendar` 自行渲染关闭按钮和标题行。 为 `true`(默认)时 `TCalendar` 渲染关闭按钮与标题行; 为 `false` 时由外层弹窗容器承载。 |
+| popupOverlayBuilder | Widget Function(BuildContext context, List selectedDates)? | - | 弹窗模式下日历内容区底部浮层构建器(非 `TPopup` 面板底部)。 由 `TCalendar.showPopup` 或手动 `TCalendarInherited` 注入; `selectedDates` 随点选实时更新。 |
+| popupOverlayExpanded | ValueListenable? | - | 浮层是否展开(响应式),需配合 `popupOverlayBuilder`。 |
+| selected | ValueNotifier> | - | 选中态的可写引用(仅供 `TCalendar` 内部更新使用)。 对外消费方请使用 `selectedListenable` 这一只读视图。 |
+| usePopup | bool? | true | - |
 
 
 ### TCalendarStyle
@@ -65,9 +119,9 @@
 
 #### 工厂构造方法
 
-##### TCalendarStyle.cellStyle
+##### TCalendarStyle.forSelectType
 
-日期样式
+按选中态生成单元格样式
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
@@ -88,15 +142,15 @@
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
 | cellDecoration | BoxDecoration? | - | 日期decoration |
-| cellPrefixStyle | TextStyle? | - | 日期前面的字符串的样式 |
-| cellStyle | TextStyle? | - | 日期样式 |
-| cellSuffixStyle | TextStyle? | - | 日期后面的字符串的样式 |
 | centreColor | Color? | - | 日期范围内背景样式 |
+| dayStyle | TextStyle? | - | 日期主区(默认阳历日数字)样式 |
 | decoration | BoxDecoration? | - | - |
 | monthTitleStyle | TextStyle? | - | body区域 年月文字样式 |
+| subtitleStyle | TextStyle? | - | 副标题样式(仅 `TCalendarDataSource.getSubtitle` 字符串路径使用) |
 | titleCloseColor | Color? | - | header区域 关闭图标的颜色 |
-| titleMaxLine | int? | - | header区域 `TCalendar.title`的行数 |
-| titleStyle | TextStyle? | - | header区域 `TCalendar.title`的样式 |
+| titleMaxLine | int? | - | header区域 `TCalendar.titleWidget`的行数 |
+| titleStyle | TextStyle? | - | header区域 `TCalendar.titleWidget`的样式 |
+| todayDayStyle | TextStyle? | - | 今天日期主区样式 |
 | weekdayStyle | TextStyle? | - | header区域 周 文字样式 |
 
 #### 公开属性
@@ -104,82 +158,50 @@
 | 属性 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
 | bodyPadding | double? | - | 月与月之间的垂直间距 |
-| todayStyle | TextStyle? | - | 当天日期样式 |
 | verticalGap | double? | - | 日期垂直间距,水平间距为`verticalGap` / 2 |
 
 
 ### TCalendarDataSource
 #### 简介
-日历数据源接口
-
-开发者需要实现此接口来提供农历转换能力。
-组件内部不包含农历算法和数据,完全依赖外部实现。
+日历可选数据源:仅提供副标题文案(无 `subtitleBuilder` 时使用)。
+农历、节气、节日等均由接入方在 `TCalendar.subtitleBuilder` 或
+`getSubtitle` 中自行处理;组件主区默认只渲染阳历日数字。
 
 #### 方法
 
 | 名称 | 返回类型 | 参数 | 说明 |
 | --- | --- | --- | --- |
-| getLunarInfo | TLunarInfo? | required DateTime solarDate | 获取指定阳历日期的农历信息 返回 null 表示不显示农历信息 |
-| formatDate | String | required DateTime date, required TCalendarDateType type, TLunarInfo? lunarInfo | 格式化日期文本 返回格式化后的日期字符串 |
-| getSolarTerm | String? | required DateTime date | 获取节气信息(可选实现) 返回节气名称,如"春分"、"秋分"等,无节气则返回 null |
-| getFestival | String? | required DateTime date, TLunarInfo? lunarInfo | 获取节日信息(可选实现) 返回节日名称,如"春节"、"中秋节"等,无节日则返回 null |
-| getHolidayInfo | Map? | required DateTime date | 获取假期信息(可选实现) 返回假期类型和名称: - 'holiday': 法定节假日/公共假期(如"国庆节") - 'workday': 调休工作日(如"补班") - null: 正常日期 示例返回值: - {'type': 'holiday', 'name': '国庆节'} - {'type': 'workday', 'name': '补班'} - null |
-| formatYear | String | required int year, required TCalendarDateType type | 格式化年份文本 返回格式化后的年份字符串 阳历示例:2025 -> "2025年" 阴历示例:2025 -> "二〇二五年" |
-| formatMonth | String | required int month, required TCalendarDateType type, bool isLeapMonth | 格式化月份文本 返回格式化后的月份字符串 阳历示例:3 -> "3月" 阴历示例:3 -> "三月",闰3月 -> "闰三月" |
-| formatDay | String | required int day, required TCalendarDateType type | 格式化日期文本 返回格式化后的日期字符串 阳历示例:7 -> "7日" 阴历示例:7 -> "初七" |
+| getSubtitle | String? | required DateTime date | 副标题文案;返回 null 或空字符串时不显示副标题行。 |
 
 
-### TLunarInfo
+### TCalendarCellModel
 #### 简介
-农历日期信息模型
+单个日期格数据(只读,选中态通过 `typeNotifier` 更新)
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| day | int | - | 农历日期(数字,1-30) |
-| dayText | String | - | 日期文本(如:初七) |
-| isLeapMonth | bool | false | 是否是闰月 |
-| month | int | - | 农历月份(数字,1-12) |
-| monthText | String | - | 月份文本(如:三月、闰三月) |
-| year | int | - | 农历年份(数字) |
-| yearText | String | - | 年份文本(如:二〇二五) |
-
-
-### TCalendarDateType
-#### 简介
-日历类型枚举
-#### 枚举值
-
-
-| 名称 | 说明 |
-| --- | --- |
-| solar | 阳历(公历) |
-| lunar | 阴历(农历) |
+| date | DateTime | - | - |
+| isLastDayOfMonth | bool | - | - |
+| typeNotifier | DateSelectTypeNotifier | - | - |
 
 
 ### CalendarType
+#### 简介
+日历选择模式
 #### 枚举值
 
 
 | 名称 | 说明 |
 | --- | --- |
-| single | - |
-| multiple | - |
-| range | - |
-
-
-### CalendarTrigger
-#### 枚举值
-
-
-| 名称 | 说明 |
-| --- | --- |
-| closeBtn | - |
-| confirmBtn | - |
-| overlay | - |
+| single | 单选:点击新日期时自动取消旧日期的选中状态 |
+| multiple | 多选:点击日期切换选中/取消,可同时选中多个日期 |
+| range | 区间选择:第一次点击选起点,第二次点击选终点,中间自动填充 |
 
 
 ### DateSelectType
+#### 简介
+日期在日历格中的选中/展示状态
 #### 枚举值
 
 
@@ -193,17 +215,21 @@
 | empty | - |
 
 
-### CalendarFormat
+### TCalendarSubtitleBuilder
+#### 简介
+副标题完全自定义
 #### 类型定义
 
 ```dart
-typedef CalendarFormat = TDate? Function(TDate? day);
+typedef TCalendarSubtitleBuilder = Widget? Function(BuildContext context, TCalendarSubtitleContext subtitleContext);
 ```
 
 
-### CalendarBuilder
+### TCalendarCellBuilder
+#### 简介
+整格自定义(主区 + 副标题均由接入方绘制)
 #### 类型定义
 
 ```dart
-typedef CalendarBuilder = Widget Function(BuildContext context);
+typedef TCalendarCellBuilder = Widget? Function(BuildContext context, TCalendarCellModel cell);
 ```
diff --git a/tdesign-site/src/action-sheet/README.md b/tdesign-site/src/action-sheet/README.md
index 2c8614a1b..54a6d1f4b 100644
--- a/tdesign-site/src/action-sheet/README.md
+++ b/tdesign-site/src/action-sheet/README.md
@@ -897,7 +897,7 @@ Widget _buildIconListLeftActionSheet(BuildContext context) {
 | badge | TBadge? | - | 角标 |
 | description | String? | - | 描述信息 |
 | disabled | bool | false | 是否禁用 |
-| group | String? | - | 分组,用于带描述多行滚动宫格 当[TActionSheet.theme]等于[TActionSheetTheme.group]时有效 有效时,如果该值未配置整个[TActionSheetItem]会被忽略,即不会展示 |
+| group | String? | - | 分组,用于带描述多行滚动宫格 当`TActionSheet.theme`等于`TActionSheetTheme.group`时有效 有效时,如果该值未配置整个`TActionSheetItem`会被忽略,即不会展示 |
 | icon | Widget? | - | 图标 |
 | iconSize | double? | - | 图标大小 |
 | label | String | - | 标题 |
@@ -907,31 +907,6 @@ Widget _buildIconListLeftActionSheet(BuildContext context) {
 ### TActionSheet
 #### 简介
 动作面板
-#### 默认构造方法
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| context | BuildContext | - | 上下文 |
-| align | TActionSheetAlign | TActionSheetAlign.center | 对齐方式 |
-| cancelText | String? | - | 取消按钮的文本 |
-| closeOnOverlayClick | bool | true | 点击蒙层时是否关闭 |
-| count | int | 8 | 每页显示的项目数 当[theme]等于[TActionSheetTheme.grid]且[showPagination]为true时有效 |
-| description | String? | - | 描述文本 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.list]时有效 |
-| itemHeight | double | 96.0 | 项目的行高 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.group]时有效 |
-| itemMinWidth | double | 80.0 | 项目的最小宽度 当[theme]等于[TActionSheetTheme.grid]且[scrollable]为true时有效 或当[theme]等于[TActionSheetTheme.group]时有效 |
-| items | List | - | ActionSheet的项目列表 |
-| onCancel | VoidCallback? | - | 取消按钮的回调函数 |
-| onClose | VoidCallback? | - | 关闭时的回调函数 |
-| onSelected | TActionSheetItemCallback? | - | 选择项目时的回调函数 |
-| rows | int | 2 | 显示的行数 当[theme]等于[TActionSheetTheme.grid]时有效 |
-| scrollable | bool | false | 是否可以横向滚动 当[theme]等于[TActionSheetTheme.grid]且[showPagination]为false时有效 |
-| showCancel | bool | true | 是否显示取消按钮 |
-| showOverlay | bool | true | 是否显示遮罩层 |
-| showPagination | bool | false | 是否显示分页 当[theme]等于[TActionSheetTheme.grid]时有效 |
-| theme | TActionSheetTheme | TActionSheetTheme.list | 主题样式 |
-| useSafeArea | bool | true | 使用安全区域 |
-| visible | bool | false | 是否立即显示 |
-
 
 #### 静态方法
 
@@ -951,14 +926,14 @@ Widget _buildIconListLeftActionSheet(BuildContext context) {
 | onSelected | TActionSheetItemCallback? | - | 选择项目时的回调函数 |
 | showOverlay | bool | true | 是否显示遮罩层 |
 | closeOnOverlayClick | bool | true | 点击蒙层时是否关闭 |
-| count | int | 8 | 每页显示的项目数 当[theme]等于[TActionSheetTheme.grid]且[showPagination]为true时有效 |
-| rows | int | 2 | 显示的行数 当[theme]等于[TActionSheetTheme.grid]时有效 |
-| itemHeight | double | 96.0 | 项目的行高 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.group]时有效 |
-| itemMinWidth | double | 80.0 | 项目的最小宽度 当[theme]等于[TActionSheetTheme.grid]且[scrollable]为true时有效 或当[theme]等于[TActionSheetTheme.group]时有效 |
-| scrollable | bool | false | 是否可以横向滚动 当[theme]等于[TActionSheetTheme.grid]且[showPagination]为false时有效 |
-| showPagination | bool | false | 是否显示分页 当[theme]等于[TActionSheetTheme.grid]时有效 |
+| count | int | 8 | 每页显示的项目数 当`theme`等于`TActionSheetTheme.grid`且`showPagination`为true时有效 |
+| rows | int | 2 | 显示的行数 当`theme`等于`TActionSheetTheme.grid`时有效 |
+| itemHeight | double | 96.0 | 项目的行高 当`theme`等于`TActionSheetTheme.grid`或`theme`等于`TActionSheetTheme.group`时有效 |
+| itemMinWidth | double | 80.0 | 项目的最小宽度 当`theme`等于`TActionSheetTheme.grid`且`scrollable`为true时有效 或当`theme`等于`TActionSheetTheme.group`时有效 |
+| scrollable | bool | false | 是否可以横向滚动 当`theme`等于`TActionSheetTheme.grid`且`showPagination`为false时有效 |
+| showPagination | bool | false | 是否显示分页 当`theme`等于`TActionSheetTheme.grid`时有效 |
 | onCancel | VoidCallback? | - | 取消按钮的回调函数 |
-| description | String? | - | 描述文本 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.list]时有效 |
+| description | String? | - | 描述文本 当`theme`等于`TActionSheetTheme.grid`或`theme`等于`TActionSheetTheme.list`时有效 |
 | onClose | VoidCallback? | - | 关闭时的回调函数 |
 | useSafeArea | bool | true | 使用安全区域 |
 
@@ -979,8 +954,8 @@ Widget _buildIconListLeftActionSheet(BuildContext context) {
 | onSelected | TActionSheetItemCallback? | - | 选择项目时的回调函数 |
 | showOverlay | bool | true | 是否显示遮罩层 |
 | closeOnOverlayClick | bool | true | 点击蒙层时是否关闭 |
-| itemHeight | double | 96.0 | 项目的行高 当[theme]等于[TActionSheetTheme.grid]或[theme]等于[TActionSheetTheme.group]时有效 |
-| itemMinWidth | double | 80.0 | 项目的最小宽度 当[theme]等于[TActionSheetTheme.grid]且[scrollable]为true时有效 或当[theme]等于[TActionSheetTheme.group]时有效 |
+| itemHeight | double | 96.0 | 项目的行高 当`theme`等于`TActionSheetTheme.grid`或`theme`等于`TActionSheetTheme.group`时有效 |
+| itemMinWidth | double | 80.0 | 项目的最小宽度 当`theme`等于`TActionSheetTheme.grid`且`scrollable`为true时有效 或当`theme`等于`TActionSheetTheme.group`时有效 |
 | onCancel | VoidCallback? | - | 取消按钮的回调函数 |
 | onClose | VoidCallback? | - | 关闭时的回调函数 |
 | useSafeArea | bool | true | 使用安全区域 |
@@ -1006,6 +981,31 @@ Widget _buildIconListLeftActionSheet(BuildContext context) {
 | onClose | VoidCallback? | - | 关闭时的回调函数 |
 | useSafeArea | bool | true | 使用安全区域 |
 
+#### 默认构造方法
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | 上下文 |
+| align | TActionSheetAlign | TActionSheetAlign.center | 对齐方式 |
+| cancelText | String? | - | 取消按钮的文本 |
+| closeOnOverlayClick | bool | true | 点击蒙层时是否关闭 |
+| count | int | 8 | 每页显示的项目数 当`theme`等于`TActionSheetTheme.grid`且`showPagination`为true时有效 |
+| description | String? | - | 描述文本 当`theme`等于`TActionSheetTheme.grid`或`theme`等于`TActionSheetTheme.list`时有效 |
+| itemHeight | double | 96.0 | 项目的行高 当`theme`等于`TActionSheetTheme.grid`或`theme`等于`TActionSheetTheme.group`时有效 |
+| itemMinWidth | double | 80.0 | 项目的最小宽度 当`theme`等于`TActionSheetTheme.grid`且`scrollable`为true时有效 或当`theme`等于`TActionSheetTheme.group`时有效 |
+| items | List | - | ActionSheet的项目列表 |
+| onCancel | VoidCallback? | - | 取消按钮的回调函数 |
+| onClose | VoidCallback? | - | 关闭时的回调函数 |
+| onSelected | TActionSheetItemCallback? | - | 选择项目时的回调函数 |
+| rows | int | 2 | 显示的行数 当`theme`等于`TActionSheetTheme.grid`时有效 |
+| scrollable | bool | false | 是否可以横向滚动 当`theme`等于`TActionSheetTheme.grid`且`showPagination`为false时有效 |
+| showCancel | bool | true | 是否显示取消按钮 |
+| showOverlay | bool | true | 是否显示遮罩层 |
+| showPagination | bool | false | 是否显示分页 当`theme`等于`TActionSheetTheme.grid`时有效 |
+| theme | TActionSheetTheme | TActionSheetTheme.list | 主题样式 |
+| useSafeArea | bool | true | 使用安全区域 |
+| visible | bool | false | 是否立即显示 |
+
 
 ### TActionSheetTheme
 #### 枚举值
diff --git a/tdesign-site/src/button/README.md b/tdesign-site/src/button/README.md
index 99fb16e95..cf19849f8 100644
--- a/tdesign-site/src/button/README.md
+++ b/tdesign-site/src/button/README.md
@@ -739,6 +739,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 ## API
 ### TButton
+#### 简介
+TD常规按钮
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -771,17 +773,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### TButtonStyle
-#### 默认构造方法
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| backgroundColor | Color? | - | 背景颜色 |
-| frameColor | Color? | - | 边框颜色 |
-| frameWidth | double? | - | 边框宽度 |
-| gradient | Gradient? | - | 渐变背景色 |
-| radius | BorderRadiusGeometry? | - | 自定义圆角 |
-| textColor | Color? | - | 文字颜色 |
-
+#### 简介
+TButton按钮样式
 
 #### 工厂构造方法
 
@@ -828,6 +821,17 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | theme | TButtonTheme? | - | - |
 | status | TButtonStatus | - | - |
 
+#### 默认构造方法
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| backgroundColor | Color? | - | 背景颜色 |
+| frameColor | Color? | - | 边框颜色 |
+| frameWidth | double? | - | 边框宽度 |
+| gradient | Gradient? | - | 渐变背景色 |
+| radius | BorderRadiusGeometry? | - | 自定义圆角 |
+| textColor | Color? | - | 文字颜色 |
+
 
 ### TButtonSize
 #### 枚举值
diff --git a/tdesign-site/src/calendar/README.md b/tdesign-site/src/calendar/README.md
index 99a89f496..1cd5b5826 100644
--- a/tdesign-site/src/calendar/README.md
+++ b/tdesign-site/src/calendar/README.md
@@ -433,20 +433,72 @@ Widget _buildLunar(BuildContext context) {
 
 ## API
 ### TCalendar
+#### 简介
+日历组件
+
+#### 静态方法
+
+##### TCalendar.showPopup
+
+弹出日历选择器,返回选中的日期列表。
+取消或关闭弹窗时返回 `null`;点击确认时返回选中的 `DateTime` 列表。
+弹窗内点选过程无 `onChange`;实时联动请用 `popupOverlayBuilder` 的 `dates`,
+或自行用 `TCalendarInherited` 监听 `TCalendarInherited.selectedListenable`。
+```dart
+final result = await TCalendar.showPopup(
+  context,
+  titleWidget: Text('请选择日期'),
+  type: CalendarType.single,
+);
+if (result != null) {
+  print('选中了: $result');
+}
+```
+若需完全自定义布局,请直接使用 `TCalendar` + `TPopup.show`
+/ `TPopupOptions.bottom` 自行组装。
+
+返回类型:`Future?>`
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | - |
+| titleWidget | Widget? | - | 标题组件,可传入 Text 或自定义 Widget |
+| type | CalendarType | CalendarType.single | 日历的选择模式,决定点击日期后的选中行为: - `CalendarType.single`:单选,点击新日期取消旧选中 - `CalendarType.multiple`:多选,点击切换选中/取消 - `CalendarType.range`:区间选择,依次选起止日期 |
+| initialValue | List? | - | 初始选中日期列表,不传则默认今天。 **非受控语义**:仅用于首次挂载;用户点选后以 `onChange` 为准,由调用方自行 `setState` 保存。若父组件在运行期修改本参数,会同步选中态并刷新格子(与 range 行为一致)。 列表长度与 `type` 对应: - `CalendarType.single`:1 个元素(选中日期) - `CalendarType.multiple`:N 个元素(所有选中日期) - `CalendarType.range`:2 个元素(起始、结束日期) |
+| minDate | DateTime? | - | 最小可选的日期,不传则默认 1970-01-01 |
+| maxDate | DateTime? | - | 最大可选的日期,不传则默认 2100-12-31 |
+| anchorDate | DateTime? | - | 锚点日期,打开时滚动到该日期所在月份。 |
+| anchorRevision | int | 0 | 锚点滚动触发序号,默认 `0`。 与 `anchorDate` 配合:序号递增可重复滚到同一月份;仅改月份时也可只更新 `anchorDate`。 |
+| popupHeight | double? | - | - |
+| firstDayOfWeek | int | 0 | 第一天从星期几开始,0 = 周日,1 = 周一,…,6 = 周六。默认 0(周日)。 |
+| cellHeight | double? | - | 日期单元格高度,默认 60。如需更大行高可传入自定义值(如 80) |
+| style | TCalendarStyle? | - | 自定义样式 |
+| popupOverlayBuilder | Widget Function(BuildContext context, List selectedDates)? | - | - |
+| popupOverlayExpanded | ValueListenable? | - | - |
+| confirmBtnBuilder | Widget Function(VoidCallback onConfirm)? | - | - |
+| onConfirm | void Function(List)? | - | - |
+| onClose | VoidCallback? | - | - |
+| onCellClick | void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? | - | 点击日期时触发 |
+| cellBuilder | TCalendarCellBuilder? | - | 整格自定义;设置后不再使用默认主区/副标题布局。 |
+| subtitleBuilder | TCalendarSubtitleBuilder? | - | 副标题完全自定义;未设置时可使用 `dataSource.getSubtitle`。 |
+| dataSource | TCalendarDataSource? | - | 可选数据源,提供副标题字符串(无 `subtitleBuilder` 时生效)。 |
+| onMonthChange | ValueChanged? | - | 月份变化时触发 |
+| monthTitleBuilder | Widget Function(BuildContext context, DateTime monthDate)? | - | 月标题构建器 |
+
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
 | anchorDate | DateTime? | - | 锚点日期,打开时滚动到该日期所在月份。 |
-| anchorRevision | int | 0 | 锚点滚动触发序号,默认 `0`。 |
+| anchorRevision | int | 0 | 锚点滚动触发序号,默认 `0`。 与 `anchorDate` 配合:序号递增可重复滚到同一月份;仅改月份时也可只更新 `anchorDate`。 |
 | animateTo | bool | false | 滚动到选中日期/锚点日期所在月份时是否使用动画,默认 false |
 | cellBuilder | TCalendarCellBuilder? | - | 整格自定义;设置后不再使用默认主区/副标题布局。 |
 | cellHeight | double? | - | 日期单元格高度,默认 60。如需更大行高可传入自定义值(如 80) |
-| dataSource | TCalendarDataSource? | - | 可选数据源,提供副标题字符串(无 [subtitleBuilder] 时生效)。 |
+| dataSource | TCalendarDataSource? | - | 可选数据源,提供副标题字符串(无 `subtitleBuilder` 时生效)。 |
 | firstDayOfWeek | int | 0 | 第一天从星期几开始,0 = 周日,1 = 周一,…,6 = 周六。默认 0(周日)。 |
 | height | double? | - | 高度,不传时内嵌模式自动按 5 行日期计算 |
-| initialValue | List? | - | 初始选中日期列表,不传则默认今天。 |
-| key |  | - |  |
+| initialValue | List? | - | 初始选中日期列表,不传则默认今天。 **非受控语义**:仅用于首次挂载;用户点选后以 `onChange` 为准,由调用方自行 `setState` 保存。若父组件在运行期修改本参数,会同步选中态并刷新格子(与 range 行为一致)。 列表长度与 `type` 对应: - `CalendarType.single`:1 个元素(选中日期) - `CalendarType.multiple`:N 个元素(所有选中日期) - `CalendarType.range`:2 个元素(起始、结束日期) |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | maxDate | DateTime? | - | 最大可选的日期,不传则默认 2100-12-31 |
 | minDate | DateTime? | - | 最小可选的日期,不传则默认 1970-01-01 |
 | monthTitleBuilder | Widget Function(BuildContext context, DateTime monthDate)? | - | 月标题构建器 |
@@ -456,48 +508,68 @@ Widget _buildLunar(BuildContext context) {
 | onMonthChange | ValueChanged? | - | 月份变化时触发 |
 | safeAreaInset | bool | true | 是否适配底部安全区域(如 iPhone Home Indicator),默认 true |
 | style | TCalendarStyle? | - | 自定义样式 |
-| subtitleBuilder | TCalendarSubtitleBuilder? | - | 副标题完全自定义;未设置时可使用 [dataSource.getSubtitle]。 |
+| subtitleBuilder | TCalendarSubtitleBuilder? | - | 副标题完全自定义;未设置时可使用 `dataSource.getSubtitle`。 |
 | titleWidget | Widget? | - | 标题组件,可传入 Text 或自定义 Widget |
-| type | CalendarType | CalendarType.single | 日历的选择模式,决定点击日期后的选中行为: |
+| type | CalendarType | CalendarType.single | 日历的选择模式,决定点击日期后的选中行为: - `CalendarType.single`:单选,点击新日期取消旧选中 - `CalendarType.multiple`:多选,点击切换选中/取消 - `CalendarType.range`:区间选择,依次选起止日期 |
 
 
+### TCalendarInherited
+#### 简介
+日历弹窗状态的 InheritedWidget 容器。
+由上层(如 `TSlidePopupRoute` 的 builder)包裹在 `TCalendar` 外侧,
+将选中态、确认/关闭回调等注入子树。
+
 #### 静态方法
 
-| 名称 | 返回类型 | 参数 | 说明 |
-| --- | --- | --- | --- |
-| showPopup |  |   required BuildContext context,  Widget? titleWidget,  CalendarType type,  List? initialValue,  DateTime? minDate,  DateTime? maxDate,  DateTime? anchorDate,  int anchorRevision,  double? popupHeight,  int firstDayOfWeek,  double? cellHeight,  TCalendarStyle? style,  Widget Function(BuildContext context, List selectedDates)? popupOverlayBuilder,  ValueListenable? popupOverlayExpanded,  Widget Function(VoidCallback onConfirm)? confirmBtnBuilder,  void Function(List)? onConfirm,  VoidCallback? onClose,  void Function(DateTime value, DateSelectType selectType, TCalendarCellModel cell)? onCellClick,  TCalendarCellBuilder? cellBuilder,  TCalendarSubtitleBuilder? subtitleBuilder,  TCalendarDataSource? dataSource,  ValueChanged? onMonthChange,  Widget Function(BuildContext context, DateTime monthDate)? monthTitleBuilder, | 弹出日历选择器,返回选中的日期列表。     取消或关闭弹窗时返回 `null`;点击确认时返回选中的 [DateTime] 列表。   弹窗内点选过程无 [onChange];实时联动请用 [popupOverlayBuilder] 的 `dates`,   或自行用 [TCalendarInherited] 监听 [TCalendarInherited.selectedListenable]。     ```dart   final result = await TCalendar.showPopup(     context,     titleWidget: Text('请选择日期'),     type: CalendarType.single,   );   if (result != null) {     print('选中了: $result');   }   ```     若需完全自定义布局,请直接使用 [TCalendar] + [TPopup.show] / [TPopupOptions.bottom] 自行组装。 |
+##### TCalendarInherited.of
 
-```
-```
+返回类型:`TCalendarInherited?`
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | - |
 
-### TCalendarInherited
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| child |  | - |  |
-| confirmBtnBuilder | Widget Function(VoidCallback onConfirm)? | - | 自定义确认按钮;[onConfirm] 与默认确认按钮一致(回传选中值并关闭弹窗)。 |
-| key |  | - |  |
-| onClose |  | - |  |
-| onConfirm |  | - |  |
-| popupOverlayBuilder | Widget Function(BuildContext context, List selectedDates)? | - | 弹窗模式下日历内容区底部浮层构建器(非 TPopup 面板底部),由 [TCalendar.showPopup] 或手动 |
-| popupOverlayExpanded | ValueListenable? | - | 浮层是否展开(响应式),需配合 [popupOverlayBuilder]。 |
-| popupConfirmBtn | bool? | - | 是否由 [TCalendar] 渲染底部确认按钮。 |
-| popupControls | bool | true | 是否由 [TCalendar] 自行渲染关闭按钮和标题行。 |
-| selected | ValueNotifier> | - | 选中态的可写引用(仅供 [TCalendar] 内部更新使用)。 |
-| usePopup |  | true |  |
+| child | Widget | - | - |
+| confirmBtnBuilder | Widget Function(VoidCallback onConfirm)? | - | 自定义确认按钮;`onConfirm` 与默认确认按钮一致(回传选中值并关闭弹窗)。 |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
+| onClose | VoidCallback? | - | - |
+| onConfirm | VoidCallback? | - | - |
+| popupConfirmBtn | bool? | - | 是否由 `TCalendar` 渲染底部确认按钮。 为 `null`(默认)时跟随 `popupControls`;显式设置时覆盖。 |
+| popupControls | bool | true | 是否由 `TCalendar` 自行渲染关闭按钮和标题行。 为 `true`(默认)时 `TCalendar` 渲染关闭按钮与标题行; 为 `false` 时由外层弹窗容器承载。 |
+| popupOverlayBuilder | Widget Function(BuildContext context, List selectedDates)? | - | 弹窗模式下日历内容区底部浮层构建器(非 `TPopup` 面板底部)。 由 `TCalendar.showPopup` 或手动 `TCalendarInherited` 注入; `selectedDates` 随点选实时更新。 |
+| popupOverlayExpanded | ValueListenable? | - | 浮层是否展开(响应式),需配合 `popupOverlayBuilder`。 |
+| selected | ValueNotifier> | - | 选中态的可写引用(仅供 `TCalendar` 内部更新使用)。 对外消费方请使用 `selectedListenable` 这一只读视图。 |
+| usePopup | bool? | true | - |
 
 
-#### 静态方法
+### TCalendarStyle
+#### 简介
+日历组件样式
 
-| 名称 | 返回类型 | 参数 | 说明 |
+#### 工厂构造方法
+
+##### TCalendarStyle.forSelectType
+
+按选中态生成单元格样式
+
+| 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| of |  |   required BuildContext context, |  |
+| context | BuildContext | - | - |
+| type | DateSelectType? | - | - |
 
-```
-```
 
-### TCalendarStyle
+##### TCalendarStyle.generateStyle
+
+生成默认样式
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | - |
+
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -505,35 +577,95 @@ Widget _buildLunar(BuildContext context) {
 | cellDecoration | BoxDecoration? | - | 日期decoration |
 | centreColor | Color? | - | 日期范围内背景样式 |
 | dayStyle | TextStyle? | - | 日期主区(默认阳历日数字)样式 |
-| decoration |  | - |  |
+| decoration | BoxDecoration? | - | - |
 | monthTitleStyle | TextStyle? | - | body区域 年月文字样式 |
-| subtitleStyle | TextStyle? | - | 副标题样式(仅 [TCalendarDataSource.getSubtitle] 字符串路径使用) |
+| subtitleStyle | TextStyle? | - | 副标题样式(仅 `TCalendarDataSource.getSubtitle` 字符串路径使用) |
 | titleCloseColor | Color? | - | header区域 关闭图标的颜色 |
-| titleMaxLine | int? | - | header区域 [TCalendar.titleWidget]的行数 |
-| titleStyle | TextStyle? | - | header区域 [TCalendar.titleWidget]的样式 |
+| titleMaxLine | int? | - | header区域 `TCalendar.titleWidget`的行数 |
+| titleStyle | TextStyle? | - | header区域 `TCalendar.titleWidget`的样式 |
 | todayDayStyle | TextStyle? | - | 今天日期主区样式 |
 | weekdayStyle | TextStyle? | - | header区域 周 文字样式 |
 
+#### 公开属性
 
-#### 工厂构造方法
-
-| 名称  | 说明 |
-| --- |  --- |
-| TCalendarStyle.forSelectType  | 按选中态生成单元格样式 |
-| TCalendarStyle.generateStyle  | 生成默认样式 |
+| 属性 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| bodyPadding | double? | - | 月与月之间的垂直间距 |
+| verticalGap | double? | - | 日期垂直间距,水平间距为`verticalGap` / 2 |
 
-```
-```
 
 ### TCalendarDataSource
-```
-```
+#### 简介
+日历可选数据源:仅提供副标题文案(无 `subtitleBuilder` 时使用)。
+农历、节气、节日等均由接入方在 `TCalendar.subtitleBuilder` 或
+`getSubtitle` 中自行处理;组件主区默认只渲染阳历日数字。
+
+#### 方法
+
+| 名称 | 返回类型 | 参数 | 说明 |
+| --- | --- | --- | --- |
+| getSubtitle | String? | required DateTime date | 副标题文案;返回 null 或空字符串时不显示副标题行。 |
+
 
 ### TCalendarCellModel
+#### 简介
+单个日期格数据(只读,选中态通过 `typeNotifier` 更新)
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| date |  | - |  |
-| isLastDayOfMonth |  | - |  |
-| typeNotifier |  | - |  |
\ No newline at end of file
+| date | DateTime | - | - |
+| isLastDayOfMonth | bool | - | - |
+| typeNotifier | DateSelectTypeNotifier | - | - |
+
+
+### CalendarType
+#### 简介
+日历选择模式
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| single | 单选:点击新日期时自动取消旧日期的选中状态 |
+| multiple | 多选:点击日期切换选中/取消,可同时选中多个日期 |
+| range | 区间选择:第一次点击选起点,第二次点击选终点,中间自动填充 |
+
+
+### DateSelectType
+#### 简介
+日期在日历格中的选中/展示状态
+#### 枚举值
+
+
+| 名称 | 说明 |
+| --- | --- |
+| selected | - |
+| disabled | - |
+| start | - |
+| centre | - |
+| end | - |
+| empty | - |
+
+
+### TCalendarSubtitleBuilder
+#### 简介
+副标题完全自定义
+#### 类型定义
+
+```dart
+typedef TCalendarSubtitleBuilder = Widget? Function(BuildContext context, TCalendarSubtitleContext subtitleContext);
+```
+
+
+### TCalendarCellBuilder
+#### 简介
+整格自定义(主区 + 副标题均由接入方绘制)
+#### 类型定义
+
+```dart
+typedef TCalendarCellBuilder = Widget? Function(BuildContext context, TCalendarCellModel cell);
+```
+
+
+  
\ No newline at end of file
diff --git a/tdesign-site/src/cell/README.md b/tdesign-site/src/cell/README.md
index d345ad750..a5a10e7a1 100644
--- a/tdesign-site/src/cell/README.md
+++ b/tdesign-site/src/cell/README.md
@@ -217,6 +217,17 @@ Widget _buildCard(BuildContext context) {
 ### TCellStyle
 #### 简介
 单元格组件样式
+
+#### 工厂构造方法
+
+##### TCellStyle.cellStyle
+
+生成单元格默认样式
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | 传递context,会生成默认样式 |
+
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -241,17 +252,6 @@ Widget _buildCard(BuildContext context) {
 | titleStyle | TextStyle? | - | 标题文字样式 |
 
 
-#### 工厂构造方法
-
-##### TCellStyle.cellStyle
-
-生成单元格默认样式
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| context | BuildContext | - | 传递context,会生成默认样式 |
-
-
 ### TCellAlign
 #### 枚举值
 
diff --git a/tdesign-site/src/collapse/README.md b/tdesign-site/src/collapse/README.md
index 0174ec848..604572e27 100644
--- a/tdesign-site/src/collapse/README.md
+++ b/tdesign-site/src/collapse/README.md
@@ -169,7 +169,22 @@ Card Style 卡片样式
 ## API
 ### TCollapse
 #### 简介
-折叠面板列表组件,需配合 [TCollapsePanel] 使用
+折叠面板列表组件,需配合 `TCollapsePanel` 使用
+
+#### 工厂构造方法
+
+##### TCollapse.accordion
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| children | List | - | 折叠面板列表的子组件 |
+| style | TCollapseStyle | TCollapseStyle.block | 折叠面板列表的样式 - `TCollapseStyle.block` 通栏风格 - `TCollapseStyle.card` 卡片风格 |
+| expansionCallback | ExpansionPanelCallback? | - | 折叠面板列表的回调函数; 回调时,入参为当前点击的折叠面板的索引 index 和是否展开的状态 isExpanded |
+| animationDuration | Duration | kThemeAnimationDuration | 折叠面板列表的动画时长 |
+| elevation | double | 0 | 折叠面板列表的阴影 |
+| initialOpenPanelValue | Object? | - | 折叠面板列表的默认展开面板的值; 当使用 `TCollapse.accordion` 时,此值生效 |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
+
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -179,28 +194,13 @@ Card Style 卡片样式
 | elevation | double | 0 | 折叠面板列表的阴影 |
 | expansionCallback | ExpansionPanelCallback? | - | 折叠面板列表的回调函数; 回调时,入参为当前点击的折叠面板的索引 index 和是否展开的状态 isExpanded |
 | key | Key? | - | 组件标识,用于区分或保留组件状态。 |
-| style | TCollapseStyle | TCollapseStyle.block | 折叠面板列表的样式 - [TCollapseStyle.block] 通栏风格 - [TCollapseStyle.card] 卡片风格 |
+| style | TCollapseStyle | TCollapseStyle.block | 折叠面板列表的样式 - `TCollapseStyle.block` 通栏风格 - `TCollapseStyle.card` 卡片风格 |
 
 #### 公开属性
 
 | 属性 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| initialOpenPanelValue | Object? | - | 折叠面板列表的默认展开面板的值; 当使用 [TCollapse.accordion] 时,此值生效 |
-
-
-#### 工厂构造方法
-
-##### TCollapse.accordion
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| children | List | - | 折叠面板列表的子组件 |
-| style | TCollapseStyle | TCollapseStyle.block | 折叠面板列表的样式 - [TCollapseStyle.block] 通栏风格 - [TCollapseStyle.card] 卡片风格 |
-| expansionCallback | ExpansionPanelCallback? | - | 折叠面板列表的回调函数; 回调时,入参为当前点击的折叠面板的索引 index 和是否展开的状态 isExpanded |
-| animationDuration | Duration | kThemeAnimationDuration | 折叠面板列表的动画时长 |
-| elevation | double | 0 | 折叠面板列表的阴影 |
-| initialOpenPanelValue | Object? | - | 折叠面板列表的默认展开面板的值; 当使用 [TCollapse.accordion] 时,此值生效 |
-| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
+| initialOpenPanelValue | Object? | - | 折叠面板列表的默认展开面板的值; 当使用 `TCollapse.accordion` 时,此值生效 |
 
 
 ### TCollapseStyle
diff --git a/tdesign-site/src/dialog/README.md b/tdesign-site/src/dialog/README.md
index 68ee496dc..355b6202e 100644
--- a/tdesign-site/src/dialog/README.md
+++ b/tdesign-site/src/dialog/README.md
@@ -704,37 +704,17 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 ## API
 ### TAlertDialog
-#### 默认构造方法
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| backgroundColor | Color? | - | 背景颜色 |
-| buttonStyle | TDialogButtonStyle | TDialogButtonStyle.normal | - |
-| buttonWidget | Widget? | - | 自定义按钮 |
-| content | String? | - | 内容 |
-| contentColor | Color? | - | 内容颜色 |
-| contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 |
-| contentWidget | Widget? | - | 内容Widget |
-| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
-| leftBtn | TDialogButtonOptions? | - | 左侧按钮配置 |
-| leftBtnAction | Function()? | - | 左侧按钮默认点击 |
-| padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 |
-| radius | double | 12.0 | 圆角 |
-| rightBtn | TDialogButtonOptions? | - | 右侧按钮配置 |
-| rightBtnAction | Function()? | - | 右侧按钮默认点击 |
-| showCloseButton | bool? | - | 显示右上角关闭按钮 |
-| title | String? | - | 标题 |
-| titleAlignment | AlignmentGeometry? | - | 标题对齐模式 |
-| titleColor | Color? | - | 标题颜色 |
-
+#### 简介
+弹窗控件
+支持横向或竖向摆放按钮
+横向最多摆放两个按钮
 
 #### 工厂构造方法
 
 ##### TAlertDialog.vertical
 
 纵向按钮排列的对话框
-
- [buttons]参数是必须的,纵向按钮默认样式都是[TButtonTheme.primary]
+`buttons`参数是必须的,纵向按钮默认样式都是`TButtonTheme.primary`
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
@@ -753,8 +733,34 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 |
 | buttonWidget | Widget? | - | 自定义按钮 |
 
+#### 默认构造方法
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| backgroundColor | Color? | - | 背景颜色 |
+| buttonStyle | TDialogButtonStyle | TDialogButtonStyle.normal | - |
+| buttonWidget | Widget? | - | 自定义按钮 |
+| content | String? | - | 内容 |
+| contentColor | Color? | - | 内容颜色 |
+| contentMaxHeight | double | 0 | 内容的最大高度,默认为0,也就是不限制高度 |
+| contentWidget | Widget? | - | 内容Widget |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
+| leftBtn | TDialogButtonOptions? | - | 左侧按钮配置 |
+| leftBtnAction | Function()? | - | 左侧按钮默认点击 |
+| padding | EdgeInsets? | const EdgeInsets.fromLTRB(24, 32, 24, 0) | 内容内边距 |
+| radius | double | 12.0 | 圆角 |
+| rightBtn | TDialogButtonOptions? | - | 右侧按钮配置 |
+| rightBtnAction | Function()? | - | 右侧按钮默认点击 |
+| showCloseButton | bool? | - | 显示右上角关闭按钮 |
+| title | String? | - | 标题 |
+| titleAlignment | AlignmentGeometry? | - | 标题对齐模式 |
+| titleColor | Color? | - | 标题颜色 |
+
 
 ### TConfirmDialog
+#### 简介
+只有一个按钮的弹窗控件
+按钮样式支持普通和文字
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -781,6 +787,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### TDialogButtonOptions
+#### 简介
+弹窗按钮配置
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -797,6 +805,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### TDialogScaffold
+#### 简介
+TDialog手脚架
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -810,6 +820,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### TDialogTitle
+#### 简介
+弹窗标题
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -820,6 +832,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### TDialogContent
+#### 简介
+弹窗内容
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -830,6 +844,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### TDialogInfoWidget
+#### 简介
+弹窗信息
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -846,6 +862,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### HorizontalNormalButtons
+#### 简介
+横向排列的两个按钮
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -856,6 +874,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### HorizontalTextButtons
+#### 简介
+左右横向文字按钮,顶部和中间有分割线
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -866,6 +886,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### TDialogButton
+#### 简介
+弹窗标题
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -885,6 +907,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### TImageDialog
+#### 简介
+带有图片的弹窗控件
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -908,6 +932,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### TInputDialog
+#### 简介
+带有输入框的弹窗
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -934,9 +960,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 ### TDialogButtonStyle
 #### 简介
 Dialog按钮样式
-
- 用于在Dialog层面配置按钮样式
- Dialog内支持配置每个按钮的样式
+用于在Dialog层面配置按钮样式
+Dialog内支持配置每个按钮的样式
 #### 枚举值
 
 
diff --git a/tdesign-site/src/drawer/README.md b/tdesign-site/src/drawer/README.md
index 1bdf26a3c..9cbce0706 100644
--- a/tdesign-site/src/drawer/README.md
+++ b/tdesign-site/src/drawer/README.md
@@ -296,7 +296,7 @@ Widget _buildBottomSimple(BuildContext context) {
 | backgroundColor | Color? | - | 组件背景颜色 |
 | bordered | bool? | true | 是否显示边框 |
 | closeOnOverlayClick | bool? | true | 点击蒙层时是否关闭抽屉 |
-| contentWidget | Widget? | - | 自定义内容,优先级高于[items]/[footer]/[title] |
+| contentWidget | Widget? | - | 自定义内容,优先级高于`items`/`footer`/`title` |
 | drawerTop | double? | - | 距离顶部的距离 |
 | footer | Widget? | - | 抽屉的底部 |
 | hover | bool? | true | 是否开启点击反馈 |
@@ -316,14 +316,14 @@ Widget _buildBottomSimple(BuildContext context) {
 ### TDrawerWidget
 #### 简介
 抽屉内容组件
- 可用于 Scaffold 中的 drawer 属性
+可用于 Scaffold 中的 drawer 属性
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
 | backgroundColor | Color? | - | 组件背景颜色 |
 | bordered | bool? | true | 是否显示边框 |
-| contentWidget | Widget? | - | 自定义内容,优先级高于[items]/[footer]/[title] |
+| contentWidget | Widget? | - | 自定义内容,优先级高于`items`/`footer`/`title` |
 | footer | Widget? | - | 抽屉的底部 |
 | hover | bool? | true | 是否开启点击反馈 |
 | isShowLastBordered | bool? | true | 是否显示最后一行分割线 |
diff --git a/tdesign-site/src/dropdown-menu/README.md b/tdesign-site/src/dropdown-menu/README.md
index cfa91abfd..23446dd01 100644
--- a/tdesign-site/src/dropdown-menu/README.md
+++ b/tdesign-site/src/dropdown-menu/README.md
@@ -275,7 +275,7 @@ TDropdownMenu _buildGroup(BuildContext context) {
 | --- | --- | --- | --- |
 | arrowColor | Color? | - | 自定义箭头颜色 |
 | arrowIcon | IconData? | - | 自定义箭头图标 |
-| builder | TDropdownItemBuilder? | - | 下拉菜单构建器,优先级高于[items] |
+| builder | TDropdownItemBuilder? | - | 下拉菜单构建器,优先级高于`items` |
 | closeOnClickOverlay | bool? | true | 是否在点击遮罩层后关闭菜单 |
 | decoration | Decoration? | - | 下拉菜单的装饰器 |
 | direction | TDropdownMenuDirection? | TDropdownMenuDirection.auto | 菜单展开方向(down、up、auto) |
@@ -288,7 +288,7 @@ TDropdownMenu _buildGroup(BuildContext context) {
 | onMenuClosed | ValueChanged? | - | 关闭菜单事件 |
 | onMenuOpened | ValueChanged? | - | 展开菜单事件 |
 | showOverlay | bool? | true | 是否显示遮罩层 |
-| tabBarAlign | MainAxisAlignment? | MainAxisAlignment.center | [TDropdownItem.label]和[arrowIcon]/[TDropdownItem.arrowIcon]的对齐方式 |
+| tabBarAlign | MainAxisAlignment? | MainAxisAlignment.center | `TDropdownItem.label`和`arrowIcon`/`TDropdownItem.arrowIcon`的对齐方式 |
 | width | double? | - | menu的宽度 |
 
 
@@ -314,9 +314,9 @@ TDropdownMenu _buildGroup(BuildContext context) {
 | onReset | VoidCallback? | - | 点击重置时触发 |
 | options | List? | const [] | 选项数据 |
 | optionsColumns | int? | 1 | 选项分栏(1-3) |
-| tabBarAlign | MainAxisAlignment? | - | [label]和[arrowIcon]/[TDropdownMenu.arrowIcon]的对齐方式 |
-| tabBarFlex | int? | 1 | 该item在menu上的宽度占比,仅在[TDropdownMenu.isScrollable]为false时有效 |
-| tabBarWidth | double? | - | 该item在menu上的宽度,仅在[TDropdownMenu.isScrollable]为true时有效 |
+| tabBarAlign | MainAxisAlignment? | - | `label`和`arrowIcon`/`TDropdownMenu.arrowIcon`的对齐方式 |
+| tabBarFlex | int? | 1 | 该item在menu上的宽度占比,仅在`TDropdownMenu.isScrollable`为false时有效 |
+| tabBarWidth | double? | - | 该item在menu上的宽度,仅在`TDropdownMenu.isScrollable`为true时有效 |
 
 #### 静态成员
 
diff --git a/tdesign-site/src/image/README.md b/tdesign-site/src/image/README.md
index ee95db5cb..e2d397620 100644
--- a/tdesign-site/src/image/README.md
+++ b/tdesign-site/src/image/README.md
@@ -1918,7 +1918,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | excludeFromSemantics | bool | false | - |
 | filterQuality | FilterQuality | FilterQuality.low | - |
 | fit | BoxFit? | - | 适配样式 |
-| frameBuilder | ImageFrameBuilder? | - | 以下系统Image属性,释义请参考系统[Image]中注释 |
+| frameBuilder | ImageFrameBuilder? | - | 以下系统Image属性,释义请参考系统`Image`中注释 |
 | gaplessPlayback | bool | false | - |
 | height | double? | - | 自定义高 |
 | imageFile | File? | - | 图片文件路径 |
diff --git a/tdesign-site/src/message/README.md b/tdesign-site/src/message/README.md
index 5ed4b412f..74773b5da 100644
--- a/tdesign-site/src/message/README.md
+++ b/tdesign-site/src/message/README.md
@@ -287,24 +287,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 ## API
 ### TMessage
-#### 默认构造方法
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| closeBtn | dynamic | - | 关闭按钮 |
-| content | String? | - | 通知内容 |
-| duration | int? | 3000 | 消息内置计时器 |
-| icon | dynamic | true | 自定义消息前面的图标 |
-| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
-| link | dynamic | - | 链接名称 |
-| marquee | MessageMarquee? | - | 跑马灯效果 |
-| offset | List? | - | 相对于 placement 的偏移量 |
-| onCloseBtnClick | VoidCallback? | - | 点击关闭按钮触发 |
-| onDurationEnd | VoidCallback? | - | 计时结束后触发 |
-| onLinkClick | VoidCallback? | - | 点击链接文本时触发 |
-| theme | MessageTheme? | MessageTheme.info | 消息组件风格 info/success/warning/error |
-| visible | bool? | true | 是否显示 |
-
+#### 简介
+TMessage 组件
 
 #### 静态方法
 
@@ -328,8 +312,28 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | onDurationEnd | VoidCallback? | - | 计时结束后触发 |
 | onLinkClick | VoidCallback? | - | 点击链接文本时触发 |
 
+#### 默认构造方法
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| closeBtn | dynamic | - | 关闭按钮 |
+| content | String? | - | 通知内容 |
+| duration | int? | 3000 | 消息内置计时器 |
+| icon | dynamic | true | 自定义消息前面的图标 |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
+| link | dynamic | - | 链接名称 |
+| marquee | MessageMarquee? | - | 跑马灯效果 |
+| offset | List? | - | 相对于 placement 的偏移量 |
+| onCloseBtnClick | VoidCallback? | - | 点击关闭按钮触发 |
+| onDurationEnd | VoidCallback? | - | 计时结束后触发 |
+| onLinkClick | VoidCallback? | - | 点击链接文本时触发 |
+| theme | MessageTheme? | MessageTheme.info | 消息组件风格 info/success/warning/error |
+| visible | bool? | true | 是否显示 |
+
 
 ### MessageMarquee
+#### 简介
+跑马灯配置
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -340,6 +344,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### MessageLink
+#### 简介
+链接设置
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
diff --git a/tdesign-site/src/notice-bar/README.md b/tdesign-site/src/notice-bar/README.md
index 6ad02fea7..d38b81b3a 100644
--- a/tdesign-site/src/notice-bar/README.md
+++ b/tdesign-site/src/notice-bar/README.md
@@ -303,7 +303,7 @@ Widget _cardNoticeBar(BuildContext context) {
 | prefixIcon | IconData? | - | 左侧图标 |
 | right | Widget? | - | 右侧内容(自定义右侧内容,优先级高于suffixIcon) |
 | speed | double? | 50 | 滚动速度 |
-| style | TNoticeBarStyle? | - | 公告栏样式 [TNoticeBarStyle] |
+| style | TNoticeBarStyle? | - | 公告栏样式 `TNoticeBarStyle` |
 | suffixIcon | IconData? | - | 右侧图标 |
 | theme | TNoticeBarTheme? | TNoticeBarTheme.info | 主题 |
 
@@ -311,17 +311,6 @@ Widget _cardNoticeBar(BuildContext context) {
 ### TNoticeBarStyle
 #### 简介
 公告栏样式
-#### 默认构造方法
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| backgroundColor | Color? | - | 公告栏背景色 |
-| context | BuildContext? | - | 上下文 |
-| leftIconColor | Color? | - | 公告栏左侧图标颜色 |
-| padding | EdgeInsetsGeometry? | - | 公告栏内边距 |
-| rightIconColor | Color? | - | 公告栏右侧图标颜色 |
-| textStyle | TextStyle? | - | 公告栏内容样式 |
-
 
 #### 工厂构造方法
 
@@ -334,6 +323,17 @@ Widget _cardNoticeBar(BuildContext context) {
 | context | BuildContext | - | 上下文 |
 | theme | TNoticeBarTheme? | TNoticeBarTheme.info | - |
 
+#### 默认构造方法
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| backgroundColor | Color? | - | 公告栏背景色 |
+| context | BuildContext? | - | 上下文 |
+| leftIconColor | Color? | - | 公告栏左侧图标颜色 |
+| padding | EdgeInsetsGeometry? | - | 公告栏内边距 |
+| rightIconColor | Color? | - | 公告栏右侧图标颜色 |
+| textStyle | TextStyle? | - | 公告栏内容样式 |
+
 
 ### TNoticeBarType
 #### 简介
diff --git a/tdesign-site/src/picker/README.md b/tdesign-site/src/picker/README.md
index f061a540b..533af80fd 100644
--- a/tdesign-site/src/picker/README.md
+++ b/tdesign-site/src/picker/README.md
@@ -508,29 +508,52 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 ## API
 ### TPicker
+#### 简介
+纯滚轮选择器组件
+数据决定形态(编译期类型安全):
+- `TPickerColumns` → 多列独立选择
+- `TPickerLinked` → 联动选择
+```dart
+// 多列独立
+TPicker(
+  items: TPickerColumns.fromRaw([['北京', '上海', '广州']]),
+)
+// 联动
+TPicker(
+  items: TPickerLinked.fromRaw({'广东': {'深圳': ['南山', '福田']}}),
+)
+```
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| cancel | Widget? | - | 工具栏左侧自定义插槽,默认使用 [TResourceDelegate.cancel] 可用于渲染图标、图标+文字组合等。点击事件依然由外层 [GestureDetector] 处理,触发 [onCancel] 回调——所以插槽内的 Widget 不需要自己处理点击。 ```dart // 简单改文字 TPicker( cancel: const Text('关闭'), onCancel: () => Navigator.of(context).pop(), ) // 带图标 TPicker( cancel: const Icon(Icons.close, size: 22), onCancel: () => Navigator.of(context).pop(), ) ``` |
-| confirm | Widget? | - | 工具栏右侧自定义插槽,默认使用 [TResourceDelegate.confirm] 可用于渲染图标、图标+文字组合等。点击事件依然由外层 [GestureDetector] 处理,触发 [onConfirm] 回调——所以插槽内的 Widget 不需要自己处理点击。 ```dart // 简单改文字 TPicker( confirm: const Text('确定'), onConfirm: (v) => Navigator.of(context).pop(v), ) // 带图标 TPicker( confirm: const Icon(Icons.check, size: 22), onConfirm: (v) => Navigator.of(context).pop(v), ) ``` |
+| cancel | Widget? | - | 工具栏左侧自定义插槽,默认使用 `TResourceDelegate.cancel` 可用于渲染图标、图标+文字组合等。点击事件依然由外层 `GestureDetector` 处理,触发 `onCancel` 回调——所以插槽内的 Widget 不需要自己处理点击。 ```dart // 简单改文字 TPicker( cancel: const Text('关闭'), onCancel: () => Navigator.of(context).pop(), ) // 带图标 TPicker( cancel: const Icon(Icons.close, size: 22), onCancel: () => Navigator.of(context).pop(), ) ``` |
+| confirm | Widget? | - | 工具栏右侧自定义插槽,默认使用 `TResourceDelegate.confirm` 可用于渲染图标、图标+文字组合等。点击事件依然由外层 `GestureDetector` 处理,触发 `onConfirm` 回调——所以插槽内的 Widget 不需要自己处理点击。 ```dart // 简单改文字 TPicker( confirm: const Text('确定'), onConfirm: (v) => Navigator.of(context).pop(v), ) // 带图标 TPicker( confirm: const Icon(Icons.check, size: 22), onConfirm: (v) => Navigator.of(context).pop(v), ) ``` |
 | disabled | bool | false | 是否禁用整个选择器(禁止滚动和操作),默认 false |
 | height | double | 200 | 视窗高度,默认 200 |
 | initialValue | List? | - | 初始选中值列表(按 value 匹配) |
 | itemBuilder | ItemBuilderType? | - | 自定义子项构建器(disabled 项仍由内部统一渲染,不会走此 builder) |
 | itemCount | int | 5 | 每屏显示 item 数,默认 5 |
 | itemDistanceCalculator | ItemDistanceCalculator? | - | 自定义距离计算器(控制颜色/字重/字号随"离中心距离"的变化) |
-| items | TPickerItems | - | 数据源(必填) 使用密封类 [TPickerItems] 编译期强制二选一: - [TPickerColumns] → 多列独立选择 - [TPickerLinked] → 联动选择 自由结构数据通过 `.fromRaw()` 工厂构造归一化。 |
+| items | TPickerItems | - | 数据源(必填) 使用密封类 `TPickerItems` 编译期强制二选一: - `TPickerColumns` → 多列独立选择 - `TPickerLinked` → 联动选择 自由结构数据通过 `.fromRaw()` 工厂构造归一化。 |
 | key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | onCancel | VoidCallback? | - | 点击「取消」按钮回调 仅作为点击事件通知,不携带任何参数。组件本身不会做任何 popup 操作,业务层可在此自行决定是否关闭弹窗、重置状态等。 |
-| onChange | void Function(TPickerValue)? | - | 值改变回调(滚动时实时触发) 触发时机: - 用户滚动经过某个 enabled 项并稳定时 - disabled 修正动画完成后,回调最终落点 **注意**:此回调代表"滚动时实时变化",不代表"用户已确认选择"。 如需"已确认"语义,请使用 [onConfirm]。 如需做网络请求/埋点等去抖处理,请在业务层自行 debounce。 |
-| onConfirm | void Function(TPickerValue)? | - | 点击「确认」按钮回调 携带当前选中的完整 [TPickerValue],包含: - `selectedOptions`: 当前选中的所有 [TPickerOption] - `values`: 各列选中项的 value 列表 - `labels`: 各列选中项的 label 列表 - `indexes`: 各列选中项的索引 与 [onChange] 不同——只有用户点击「确认」时才触发,代表"已确认选择"。 组件本身不会做任何 popup 操作,业务层可在此自行决定是否关闭弹窗、 提交表单等。 |
-| onLoad | void Function(TPickerLoadEvent)? | - | 列选中项变化的事件回调 **触发时机**:每次用户滚动到一个 enabled 项后都会触发(联动模式下还会 在新展开的列就位后触发)。组件本身不做"距底部多少项"的阈值判断——把 决策权交给业务层。 **事件参数**包含: - [TPickerLoadEvent.column]:触发列索引 - [TPickerLoadEvent.remaining]:当前列距底部剩余项数 - [TPickerLoadEvent.displayedCount]:当前列总项数 - [TPickerLoadEvent.parentValue]:联动模式下父级选中值(首列为 null) **典型用法**:业务层根据 [TPickerLoadEvent.remaining] 自行判断是否加载更多。 ```dart onLoad: (e) async { if (e.remaining > 5 \|\| _isLoading) return; // 距底部还远 / 已在加载,跳过 _isLoading = true; final more = await fetchMore(parent: e.parentValue); setState(() { _data.addAll(more); _isLoading = false; }); } ``` |
-| title | String? | - | 工具栏中部标题(可选,不传时中部留白) 顶部工具栏永远显示,包含「取消」「标题」「确认」三块。 用户点击「取消」触发 [onCancel],点击「确认」触发 [onConfirm]。 选择器与弹窗(popup)完全解耦——关闭/打开弹窗的逻辑由业务层在 这两个回调中自行控制。 典型用法(与 popup 弹窗组合): ```dart TPicker( items: items, title: '请选择地区', onCancel: () => setState(() => visible = false), onConfirm: (value) { setState(() { selected = value; visible = false; }); }, ) ``` |
-| titleWidget | Widget? | - | 工具栏中部自定义标题插槽 传入后会**完全替换**默认的 [title] 文字,可用于渲染更复杂的标题(副标题、图标+文字等)。 标题区域不响应点击。 |
+| onChange | void Function(TPickerValue)? | - | 值改变回调(滚动时实时触发) 触发时机: - 用户滚动经过某个 enabled 项并稳定时 - disabled 修正动画完成后,回调最终落点 **注意**:此回调代表"滚动时实时变化",不代表"用户已确认选择"。 如需"已确认"语义,请使用 `onConfirm`。 如需做网络请求/埋点等去抖处理,请在业务层自行 debounce。 |
+| onConfirm | void Function(TPickerValue)? | - | 点击「确认」按钮回调 携带当前选中的完整 `TPickerValue`,包含: - `selectedOptions`: 当前选中的所有 `TPickerOption` - `values`: 各列选中项的 value 列表 - `labels`: 各列选中项的 label 列表 - `indexes`: 各列选中项的索引 与 `onChange` 不同——只有用户点击「确认」时才触发,代表"已确认选择"。 组件本身不会做任何 popup 操作,业务层可在此自行决定是否关闭弹窗、 提交表单等。 |
+| onLoad | void Function(TPickerLoadEvent)? | - | 列选中项变化的事件回调 **触发时机**:每次用户滚动到一个 enabled 项后都会触发(联动模式下还会 在新展开的列就位后触发)。组件本身不做"距底部多少项"的阈值判断——把 决策权交给业务层。 **事件参数**包含: - `TPickerLoadEvent.column`:触发列索引 - `TPickerLoadEvent.remaining`:当前列距底部剩余项数 - `TPickerLoadEvent.displayedCount`:当前列总项数 - `TPickerLoadEvent.parentValue`:联动模式下父级选中值(首列为 null) **典型用法**:业务层根据 `TPickerLoadEvent.remaining` 自行判断是否加载更多。 ```dart onLoad: (e) async { if (e.remaining > 5 \|\| _isLoading) return; // 距底部还远 / 已在加载,跳过 _isLoading = true; final more = await fetchMore(parent: e.parentValue); setState(() { _data.addAll(more); _isLoading = false; }); } ``` |
+| title | String? | - | 工具栏中部标题(可选,不传时中部留白) 顶部工具栏永远显示,包含「取消」「标题」「确认」三块。 用户点击「取消」触发 `onCancel`,点击「确认」触发 `onConfirm`。 选择器与弹窗(popup)完全解耦——关闭/打开弹窗的逻辑由业务层在 这两个回调中自行控制。 典型用法(与 popup 弹窗组合): ```dart TPicker( items: items, title: '请选择地区', onCancel: () => setState(() => visible = false), onConfirm: (value) { setState(() { selected = value; visible = false; }); }, ) ``` |
+| titleWidget | Widget? | - | 工具栏中部自定义标题插槽 传入后会**完全替换**默认的 `title` 文字,可用于渲染更复杂的标题(副标题、图标+文字等)。 标题区域不响应点击。 |
 
 
 ### TPickerOption
+#### 简介
+选择器选项
+label 用于显示,value 用于 onChange 返回,两者分离以便自定义展示
+(emoji、单位、国际化)同时保持纯净的业务值。
+```dart
+TPickerOption(label: '👨 男性', value: 'M')
+TPickerOption(label: '广东省', value: 'GD', disabled: true)
+```
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -541,6 +564,12 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### TPickerValue
+#### 简介
+onChange 回调返回的选中信息
+```dart
+onChange: (v) => setState(() => _lastValue = v);
+Text(_lastValue?.labels.join(' / ') ?? '');
+```
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -557,6 +586,12 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### TPickerLoadEvent
+#### 简介
+列选中变化的事件参数
+每当用户在某一列滚动到一个 enabled 项后,`TPicker.onLoad` 会收到一个该事件实例。
+事件里携带了"列索引、当前列总数、距底部剩余项数、联动模式下父级选中值"等
+上下文信息,业务层据此自行决定是否加载更多数据(例如:
+`if (e.remaining > 5) return;`)。
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -568,68 +603,95 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 
 ### TPickerColumns
-#### 默认构造方法
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| columns | List> | - | 每列的选项列表 |
-
+#### 简介
+多列独立选择的数据源
+```dart
+TPicker(
+  items: TPickerColumns([
+    [TPickerOption(label: '北京', value: 'BJ'), ...],
+    [TPickerOption(label: '朝阳区', value: 'CY'), ...],
+  ]),
+)
+```
 
 #### 工厂构造方法
 
 ##### TPickerColumns.fromRaw
 
 从自由结构的 raw 数据创建,自动归一化
-
- ```dart
- TPickerColumns.fromRaw(
-   [['北京', '上海', '广州']],
-   keys: const TPickerKeys(label: 'name', value: 'code'),
- )
- ```
+```dart
+TPickerColumns.fromRaw(
+  [['北京', '上海', '广州']],
+  keys: const TPickerKeys(label: 'name', value: 'code'),
+)
+```
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
 | rawColumns | List | - | - |
 | keys | TPickerKeys | TPickerKeys.defaults | - |
 
-
-### TPickerLinked
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| tree | Map | - | 联动树结构:`Map` value 可以是: - `Map` → 下一级联动 - `List` → 叶子级选项 |
+| columns | List> | - | 每列的选项列表 |
 
 
+### TPickerLinked
+#### 简介
+联动选择的数据源
+```dart
+TPicker(
+  items: TPickerLinked({
+    TPickerOption(label: '广东', value: 'GD'): {
+      TPickerOption(label: '深圳', value: 'SZ'): [
+        TPickerOption(label: '南山', value: 'NS'),
+      ],
+    },
+  }),
+)
+```
+
 #### 工厂构造方法
 
 ##### TPickerLinked.fromRaw
 
 从自由结构的 raw Map 数据创建,自动归一化
-
- ```dart
- TPickerLinked.fromRaw({
-   '广东': {'深圳': ['南山', '福田'], '广州': ['天河']},
-   '浙江': {'杭州': ['西湖']},
- })
- ```
+```dart
+TPickerLinked.fromRaw({
+  '广东': {'深圳': ['南山', '福田'], '广州': ['天河']},
+  '浙江': {'杭州': ['西湖']},
+})
+```
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
 | rawTree | Map | - | - |
 | keys | TPickerKeys | TPickerKeys.defaults | - |
 
+#### 默认构造方法
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| tree | Map | - | 联动树结构:`Map` value 可以是: - `Map` → 下一级联动 - `List` → 叶子级选项 |
+
 
 ### TPickerItems
 #### 简介
 选择器数据源密封类
-
- 编译期强制二选一,消除运行时类型错误:
- - [TPickerColumns] → 多列独立选择
- - [TPickerLinked] → 联动选择
+编译期强制二选一,消除运行时类型错误:
+- `TPickerColumns` → 多列独立选择
+- `TPickerLinked` → 联动选择
 
 ### TPickerKeys
+#### 简介
+字段映射配置
+当 picker 数据源不是 `TPickerOption` 时,用于声明原始结构中的字段名。
+```dart
+// 数据:[{ id: '1', name: '选项A', readonly: false }]
+const keys = TPickerKeys(label: 'name', value: 'id', disabled: 'readonly');
+```
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
diff --git a/tdesign-site/src/popup/README.md b/tdesign-site/src/popup/README.md
index b9588e4f2..3a0016819 100644
--- a/tdesign-site/src/popup/README.md
+++ b/tdesign-site/src/popup/README.md
@@ -541,345 +541,232 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 ### TPopup
 #### 简介
 弹出层入口:五向滑入 / 居中弹出,支持蒙层、bottom 操作区、center 面板外下方关闭区。
+通过 `show` 命令式打开;返回 `TPopupHandle` 用于关闭与再次打开。
+多次调用 `show` 会继续压入新的浮层路由,可用于叠加展示。
 
- 通过 [show] 命令式打开;返回 [TPopupHandle] 用于关闭与再次打开。
- 多次调用 [show] 会继续压入新的浮层路由,可用于叠加展示。
-
- **示例**
-
- ```dart
- final handle = TPopup.show(
-   context,
-   options: TPopupOptions.bottom(
-     titleWidget: const Text('标题'),
-     child: MyPanel(),
-   ),
- );
- handle.close();
- handle.open();
- ```
-
- 配置项见 [TPopupOptions];方向见 [TPopupPlacement]。
-
-#### 工厂构造方法
-
-##### TPopup._
+配置项见 `TPopupOptions`;方向见 `TPopupPlacement`。
 
 #### 静态方法
 
 ##### TPopup.show
 
-打开浮层并压入独立 [PopupRoute]。
-
- [context] 用于查找 [Navigator] 并展示浮层。
-
- [options] 浮层配置;方向固定时推荐 [TPopupOptions.bottom] 等命名工厂。
-
- 返回 [TPopupHandle],可用 [TPopupHandle.close]、[TPopupHandle.open]、
- [TPopupHandle.isShowing] 控制与查询。
- 重复调用会继续 push 新的浮层;若需互斥请在业务层管理。
-
- [navigatorContext] 可选,指定承载浮层的 [Navigator] 的 context,默认 [context]。
-
- [useRootNavigator] 为 true 时使用根 [Navigator](嵌套导航场景)。
+打开浮层并压入独立 `PopupRoute`。
+返回 `TPopupHandle`,可用 `TPopupHandle.close`、`TPopupHandle.open`、
+`TPopupHandle.isShowing` 控制与查询。
+重复调用会继续 push 新的浮层;若需互斥请在业务层管理。
 
 返回类型:`TPopupHandle`
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| context | BuildContext | - | - |
-| options | TPopupOptions | - | 创建时传入的配置;每次 [open] 会按 [TPopupOptions.placement] 裁剪无效字段后使用。 |
-| navigatorContext | BuildContext? | - | 与 [TPopup.show] 的 [navigatorContext] 相同。 |
-| useRootNavigator | bool | false | 与 [TPopup.show] 的 [useRootNavigator] 相同。 |
+| context | BuildContext | - | 用于查找 `Navigator` 并展示浮层。 |
+| options | TPopupOptions | - | 浮层配置;方向固定时推荐 `TPopupOptions.bottom` 等命名工厂。 |
+| navigatorContext | BuildContext? | - | 可选,指定承载浮层的 `Navigator` 的 context,默认 `context`。 |
+| useRootNavigator | bool | false | 为 true 时使用根 `Navigator`(嵌套导航场景)。 |
 
 
 ### TPopupOptions
 #### 简介
-[TPopup.show] 的配置对象。
+`TPopup.show` 的配置对象。
+## 如何创建
+| 场景 | 推荐用法 |
+|------|----------|
+| 弹出方向已知 | `TPopupOptions.bottom`、`TPopupOptions.center`、`TPopupOptions.top`、`TPopupOptions.left`、`TPopupOptions.right` |
+| 方向由变量决定 | 默认构造并设置 `placement`;传错字段会在 `TPopup.show` / `TPopupHandle.open` 时抛 `FlutterError` |
+命名工厂只暴露当前方向生效的字段(例如 `TPopupOptions.bottom` 无 `width` 参数)。
+## 字段与 `TPopupPlacement`
+| `TPopupPlacement` | 头部 / 关闭区 | 尺寸 |
+|-------------------|-------------|------|
+| `TPopupPlacement.bottom` | `headerBuilder`、`titleWidget`、`cancelBuilder`、`confirmBuilder` | `height`、`inset` |
+| `TPopupPlacement.center` | `closeBuilder` | `width`、`height` |
+| `TPopupPlacement.top` | — | `height`、`inset` |
+| `TPopupPlacement.left`、`TPopupPlacement.right` | — | `width`、`inset` |
+## Builder 三态(`headerBuilder`、`cancelBuilder`、`confirmBuilder`、`closeBuilder`)
+| 传参方式 | 效果 |
+|----------|------|
+| 省略(使用默认值) | 渲染内置 UI |
+| 显式 `null` | 隐藏该区域 |
+| 自定义 `TPopupHeaderBuilder` / `TPopupSlotBuilder` | 完全替换;需自行提供交互与语义,可调用 `close` 关闭浮层 |
+`titleWidget` 默认为 `null`,表示无标题内容。
+生命周期回调见 `onOpen`、`onOpened`、`onClose`、`onClosed`、`onVisibleChange`、`onOverlayClick`。
 
- ## 如何创建
-
- | 场景 | 推荐用法 |
- |------|----------|
- | 弹出方向已知 | [TPopupOptions.bottom]、[TPopupOptions.center]、[TPopupOptions.top]、[TPopupOptions.left]、[TPopupOptions.right] |
- | 方向由变量决定 | 默认构造并设置 [placement];传错字段会在 [TPopup.show] / [TPopupHandle.open] 时抛 [FlutterError] |
-
- 命名工厂只暴露当前方向生效的字段(例如 [TPopupOptions.bottom] 无 [width] 参数)。
-
- ## 字段与 [TPopupPlacement]
-
- | [TPopupPlacement] | 头部 / 关闭区 | 尺寸 |
- |-------------------|-------------|------|
- | [TPopupPlacement.bottom] | [headerBuilder]、[titleWidget]、[cancelBuilder]、[confirmBuilder] | [height]、[inset] |
- | [TPopupPlacement.center] | [closeBuilder] | [width]、[height] |
- | [TPopupPlacement.top] | — | [height]、[inset] |
- | [TPopupPlacement.left]、[TPopupPlacement.right] | — | [width]、[inset] |
-
- ## Builder 三态([headerBuilder]、[cancelBuilder]、[confirmBuilder]、[closeBuilder])
-
- | 传参方式 | 效果 |
- |----------|------|
- | 省略(使用默认值) | 渲染内置 UI |
- | 显式 `null` | 隐藏该区域 |
- | 自定义 [TPopupHeaderBuilder] / [TPopupSlotBuilder] | 完全替换;需自行提供交互与语义,可调用 `close` 关闭浮层 |
+#### 工厂构造方法
 
- [titleWidget] 默认为 `null`,表示无标题内容。
+##### 通用参数
 
- 生命周期回调见 [onOpen]、[onOpened]、[onClose]、[onClosed]、[onVisibleChange]、[onOverlayClick]。
-#### 默认构造方法
+以下参数由各命名工厂统一透传,含义一致:
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
 | animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 |
 | backgroundColor | Color? | - | 内容区背景色,默认主题容器色。 |
-| cancelBuilder | TPopupSlotBuilder? | _kPopupDefaultCancel | bottom 左侧操作槽;仅 [headerBuilder] 为内置默认时生效。 内置默认为「取消」,点击触发 [TPopupTrigger.cancel]。 |
 | child | Widget | - | 浮层主体内容(必填)。 |
-| closeBuilder | TPopupSlotBuilder? | _kPopupDefaultClose | center 面板外下方关闭区;仅 [TPopupPlacement.center] 生效。三态见类文档「Builder 三态」。 内置默认点击触发 [TPopupTrigger.close]。 |
 | closeOnOverlayClick | bool? | - | - |
-| confirmBuilder | TPopupSlotBuilder? | _kPopupDefaultConfirm | bottom 右侧操作槽;仅 [headerBuilder] 为内置默认时生效。 内置默认为「确定」,点击触发 [TPopupTrigger.confirm]。 |
 | destroyOnClose | bool | false | 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 |
-| headerBuilder | TPopupHeaderBuilder? | _kPopupDefaultHeader | bottom 头部;仅 [TPopupPlacement.bottom] 生效。三态见类文档「Builder 三态」。 自定义时忽略 [titleWidget]、[cancelBuilder]、[confirmBuilder]。 |
-| height | double? | - | 高度;[TPopupPlacement.top]、[TPopupPlacement.bottom] 生效;[TPopupPlacement.center] 约束面板尺寸。 |
-| inset | TPopupInset? | - | 交叉轴边缘留白;具体类型由 [placement] 决定。 * [TPopupPlacement.bottom] 使用 [TPopupBottomInset] * [TPopupPlacement.top] 使用 [TPopupTopInset] * [TPopupPlacement.left] 使用 [TPopupLeftInset] * [TPopupPlacement.right] 使用 [TPopupRightInset] * [TPopupPlacement.center] 不支持 |
-| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 [showOverlay] 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
-| onClose | VoidCallback? | - | 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 |
+| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 `showOverlay` 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
+| onClose | VoidCallback? | - | 开始关闭(与 `onVisibleChange` 的 `visible: false` 同期)。 |
 | onClosed | VoidCallback? | - | 当前展示周期真正结束。 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 |
 | onOpen | VoidCallback? | - | 路由 push 时(打开动画开始前)。 |
 | onOpened | VoidCallback? | - | 打开动画结束。 |
-| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 |
-| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 [TPopupTrigger]。 |
+| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 `closeOnOverlayClick`。 |
+| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 `TPopupTrigger`。 |
 | overlayColor | Color? | - | 蒙层颜色,默认 black54。 |
-| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 |
-| placement | TPopupPlacement | TPopupPlacement.bottom | 出现位置,默认 [TPopupPlacement.bottom]。 |
+| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 `overlayColor` 的 alpha 相乘后用于绘制。 |
 | radius | double? | - | 内容区圆角,默认主题大圆角。 |
-| showOverlay | bool | true | 是否绘制半透明蒙层。 当 [modal] 为 true 且此值为 false 时,为“透明模态弹层”。 |
-| titleWidget | Widget? | - | bottom 标题插槽;仅 [headerBuilder] 为内置默认时生效。`null` 表示无标题。 |
-| width | double? | - | 宽度;[TPopupPlacement.left]、[TPopupPlacement.right]、[TPopupPlacement.center] 生效。 |
+| showOverlay | bool | true | 是否绘制半透明蒙层。 当 `modal` 为 true 且此值为 false 时,为“透明模态弹层”。 |
 
 
-#### 工厂构造方法
-
 ##### TPopupOptions.bottom
 
-创建 [TPopupPlacement.bottom] 配置。
+创建 `TPopupPlacement.bottom` 配置。
+固定 `placement` 为 `TPopupPlacement.bottom`;默认带内置头部。
+蒙层、动画、生命周期等字段语义见同名成员文档。
 
- 固定 [placement] 为 [TPopupPlacement.bottom];默认带内置头部。
- 蒙层、动画、生命周期等字段语义见同名成员文档。
+其余参数见「通用参数」。
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| child | Widget | - | 浮层主体内容(必填)。 |
-| height | double? | - | 高度;[TPopupPlacement.top]、[TPopupPlacement.bottom] 生效;[TPopupPlacement.center] 约束面板尺寸。 |
-| inset | TPopupBottomInset? | - | 交叉轴边缘留白;具体类型由 [placement] 决定。 * [TPopupPlacement.bottom] 使用 [TPopupBottomInset] * [TPopupPlacement.top] 使用 [TPopupTopInset] * [TPopupPlacement.left] 使用 [TPopupLeftInset] * [TPopupPlacement.right] 使用 [TPopupRightInset] * [TPopupPlacement.center] 不支持 |
-| headerBuilder | TPopupHeaderBuilder? | _kPopupDefaultHeader | bottom 头部;仅 [TPopupPlacement.bottom] 生效。三态见类文档「Builder 三态」。 自定义时忽略 [titleWidget]、[cancelBuilder]、[confirmBuilder]。 |
-| titleWidget | Widget? | - | bottom 标题插槽;仅 [headerBuilder] 为内置默认时生效。`null` 表示无标题。 |
-| cancelBuilder | TPopupSlotBuilder? | _kPopupDefaultCancel | bottom 左侧操作槽;仅 [headerBuilder] 为内置默认时生效。 内置默认为「取消」,点击触发 [TPopupTrigger.cancel]。 |
-| confirmBuilder | TPopupSlotBuilder? | _kPopupDefaultConfirm | bottom 右侧操作槽;仅 [headerBuilder] 为内置默认时生效。 内置默认为「确定」,点击触发 [TPopupTrigger.confirm]。 |
-| radius | double? | - | 内容区圆角,默认主题大圆角。 |
-| backgroundColor | Color? | - | 内容区背景色,默认主题容器色。 |
-| showOverlay | bool | true | 是否绘制半透明蒙层。 当 [modal] 为 true 且此值为 false 时,为“透明模态弹层”。 |
-| closeOnOverlayClick | bool? | - | - |
-| overlayColor | Color? | - | 蒙层颜色,默认 black54。 |
-| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 |
-| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 [showOverlay] 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
-| destroyOnClose | bool | false | 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 |
-| animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 |
-| onOpen | VoidCallback? | - | 路由 push 时(打开动画开始前)。 |
-| onOpened | VoidCallback? | - | 打开动画结束。 |
-| onClose | VoidCallback? | - | 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 |
-| onClosed | VoidCallback? | - | 当前展示周期真正结束。 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 |
-| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 [TPopupTrigger]。 |
-| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 |
+| height | double? | - | 高度;`TPopupPlacement.top`、`TPopupPlacement.bottom` 生效;`TPopupPlacement.center` 约束面板尺寸。 |
+| inset | TPopupBottomInset? | - | 交叉轴边缘留白;具体类型由 `placement` 决定。 * `TPopupPlacement.bottom` 使用 `TPopupBottomInset` * `TPopupPlacement.top` 使用 `TPopupTopInset` * `TPopupPlacement.left` 使用 `TPopupLeftInset` * `TPopupPlacement.right` 使用 `TPopupRightInset` * `TPopupPlacement.center` 不支持 |
+| headerBuilder | TPopupHeaderBuilder? | _kPopupDefaultHeader | bottom 头部;仅 `TPopupPlacement.bottom` 生效。三态见类文档「Builder 三态」。 自定义时忽略 `titleWidget`、`cancelBuilder`、`confirmBuilder`。 |
+| titleWidget | Widget? | - | bottom 标题插槽;仅 `headerBuilder` 为内置默认时生效。`null` 表示无标题。 |
+| cancelBuilder | TPopupSlotBuilder? | _kPopupDefaultCancel | bottom 左侧操作槽;仅 `headerBuilder` 为内置默认时生效。 内置默认为「取消」,点击触发 `TPopupTrigger.cancel`。 |
+| confirmBuilder | TPopupSlotBuilder? | _kPopupDefaultConfirm | bottom 右侧操作槽;仅 `headerBuilder` 为内置默认时生效。 内置默认为「确定」,点击触发 `TPopupTrigger.confirm`。 |
 
 
 ##### TPopupOptions.center
 
-创建 [TPopupPlacement.center] 配置。
+创建 `TPopupPlacement.center` 配置。
+固定 `placement` 为 `TPopupPlacement.center`;默认展示面板外下方圆形关闭按钮。
 
- 固定 [placement] 为 [TPopupPlacement.center];默认展示面板外下方圆形关闭按钮。
+其余参数见「通用参数」。
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| child | Widget | - | 浮层主体内容(必填)。 |
-| width | double? | - | 宽度;[TPopupPlacement.left]、[TPopupPlacement.right]、[TPopupPlacement.center] 生效。 |
-| height | double? | - | 高度;[TPopupPlacement.top]、[TPopupPlacement.bottom] 生效;[TPopupPlacement.center] 约束面板尺寸。 |
-| closeBuilder | TPopupSlotBuilder? | _kPopupDefaultClose | center 面板外下方关闭区;仅 [TPopupPlacement.center] 生效。三态见类文档「Builder 三态」。 内置默认点击触发 [TPopupTrigger.close]。 |
-| radius | double? | - | 内容区圆角,默认主题大圆角。 |
-| backgroundColor | Color? | - | 内容区背景色,默认主题容器色。 |
-| showOverlay | bool | true | 是否绘制半透明蒙层。 当 [modal] 为 true 且此值为 false 时,为“透明模态弹层”。 |
-| closeOnOverlayClick | bool? | - | - |
-| overlayColor | Color? | - | 蒙层颜色,默认 black54。 |
-| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 |
-| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 [showOverlay] 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
-| destroyOnClose | bool | false | 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 |
-| animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 |
-| onOpen | VoidCallback? | - | 路由 push 时(打开动画开始前)。 |
-| onOpened | VoidCallback? | - | 打开动画结束。 |
-| onClose | VoidCallback? | - | 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 |
-| onClosed | VoidCallback? | - | 当前展示周期真正结束。 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 |
-| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 [TPopupTrigger]。 |
-| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 |
+| width | double? | - | 宽度;`TPopupPlacement.left`、`TPopupPlacement.right`、`TPopupPlacement.center` 生效。 |
+| height | double? | - | 高度;`TPopupPlacement.top`、`TPopupPlacement.bottom` 生效;`TPopupPlacement.center` 约束面板尺寸。 |
+| closeBuilder | TPopupSlotBuilder? | _kPopupDefaultClose | center 面板外下方关闭区;仅 `TPopupPlacement.center` 生效。三态见类文档「Builder 三态」。 内置默认点击触发 `TPopupTrigger.close`。 |
 
 
 ##### TPopupOptions.left
 
-创建 [TPopupPlacement.left] 配置。
+创建 `TPopupPlacement.left` 配置。
+固定 `placement` 为 `TPopupPlacement.left`;未传 `width` 时布局默认宽度 280。
 
- 固定 [placement] 为 [TPopupPlacement.left];未传 [width] 时布局默认宽度 280。
+其余参数见「通用参数」。
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| child | Widget | - | 浮层主体内容(必填)。 |
-| width | double? | - | 宽度;[TPopupPlacement.left]、[TPopupPlacement.right]、[TPopupPlacement.center] 生效。 |
-| inset | TPopupLeftInset? | - | 交叉轴边缘留白;具体类型由 [placement] 决定。 * [TPopupPlacement.bottom] 使用 [TPopupBottomInset] * [TPopupPlacement.top] 使用 [TPopupTopInset] * [TPopupPlacement.left] 使用 [TPopupLeftInset] * [TPopupPlacement.right] 使用 [TPopupRightInset] * [TPopupPlacement.center] 不支持 |
-| radius | double? | - | 内容区圆角,默认主题大圆角。 |
-| backgroundColor | Color? | - | 内容区背景色,默认主题容器色。 |
-| showOverlay | bool | true | 是否绘制半透明蒙层。 当 [modal] 为 true 且此值为 false 时,为“透明模态弹层”。 |
-| closeOnOverlayClick | bool? | - | - |
-| overlayColor | Color? | - | 蒙层颜色,默认 black54。 |
-| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 |
-| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 [showOverlay] 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
-| destroyOnClose | bool | false | 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 |
-| animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 |
-| onOpen | VoidCallback? | - | 路由 push 时(打开动画开始前)。 |
-| onOpened | VoidCallback? | - | 打开动画结束。 |
-| onClose | VoidCallback? | - | 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 |
-| onClosed | VoidCallback? | - | 当前展示周期真正结束。 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 |
-| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 [TPopupTrigger]。 |
-| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 |
+| width | double? | - | 宽度;`TPopupPlacement.left`、`TPopupPlacement.right`、`TPopupPlacement.center` 生效。 |
+| inset | TPopupLeftInset? | - | 交叉轴边缘留白;具体类型由 `placement` 决定。 * `TPopupPlacement.bottom` 使用 `TPopupBottomInset` * `TPopupPlacement.top` 使用 `TPopupTopInset` * `TPopupPlacement.left` 使用 `TPopupLeftInset` * `TPopupPlacement.right` 使用 `TPopupRightInset` * `TPopupPlacement.center` 不支持 |
 
 
 ##### TPopupOptions.right
 
-创建 [TPopupPlacement.right] 配置。
+创建 `TPopupPlacement.right` 配置。
+固定 `placement` 为 `TPopupPlacement.right`;未传 `width` 时布局默认宽度 280。
 
- 固定 [placement] 为 [TPopupPlacement.right];未传 [width] 时布局默认宽度 280。
+其余参数见「通用参数」。
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| child | Widget | - | 浮层主体内容(必填)。 |
-| width | double? | - | 宽度;[TPopupPlacement.left]、[TPopupPlacement.right]、[TPopupPlacement.center] 生效。 |
-| inset | TPopupRightInset? | - | 交叉轴边缘留白;具体类型由 [placement] 决定。 * [TPopupPlacement.bottom] 使用 [TPopupBottomInset] * [TPopupPlacement.top] 使用 [TPopupTopInset] * [TPopupPlacement.left] 使用 [TPopupLeftInset] * [TPopupPlacement.right] 使用 [TPopupRightInset] * [TPopupPlacement.center] 不支持 |
-| radius | double? | - | 内容区圆角,默认主题大圆角。 |
-| backgroundColor | Color? | - | 内容区背景色,默认主题容器色。 |
-| showOverlay | bool | true | 是否绘制半透明蒙层。 当 [modal] 为 true 且此值为 false 时,为“透明模态弹层”。 |
-| closeOnOverlayClick | bool? | - | - |
-| overlayColor | Color? | - | 蒙层颜色,默认 black54。 |
-| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 |
-| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 [showOverlay] 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
-| destroyOnClose | bool | false | 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 |
-| animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 |
-| onOpen | VoidCallback? | - | 路由 push 时(打开动画开始前)。 |
-| onOpened | VoidCallback? | - | 打开动画结束。 |
-| onClose | VoidCallback? | - | 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 |
-| onClosed | VoidCallback? | - | 当前展示周期真正结束。 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 |
-| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 [TPopupTrigger]。 |
-| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 |
+| width | double? | - | 宽度;`TPopupPlacement.left`、`TPopupPlacement.right`、`TPopupPlacement.center` 生效。 |
+| inset | TPopupRightInset? | - | 交叉轴边缘留白;具体类型由 `placement` 决定。 * `TPopupPlacement.bottom` 使用 `TPopupBottomInset` * `TPopupPlacement.top` 使用 `TPopupTopInset` * `TPopupPlacement.left` 使用 `TPopupLeftInset` * `TPopupPlacement.right` 使用 `TPopupRightInset` * `TPopupPlacement.center` 不支持 |
 
 
 ##### TPopupOptions.top
 
-创建 [TPopupPlacement.top] 配置。
+创建 `TPopupPlacement.top` 配置。
+固定 `placement` 为 `TPopupPlacement.top`;无内置头部。
 
- 固定 [placement] 为 [TPopupPlacement.top];无内置头部。
+其余参数见「通用参数」。
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| child | Widget | - | 浮层主体内容(必填)。 |
-| height | double? | - | 高度;[TPopupPlacement.top]、[TPopupPlacement.bottom] 生效;[TPopupPlacement.center] 约束面板尺寸。 |
-| inset | TPopupTopInset? | - | 交叉轴边缘留白;具体类型由 [placement] 决定。 * [TPopupPlacement.bottom] 使用 [TPopupBottomInset] * [TPopupPlacement.top] 使用 [TPopupTopInset] * [TPopupPlacement.left] 使用 [TPopupLeftInset] * [TPopupPlacement.right] 使用 [TPopupRightInset] * [TPopupPlacement.center] 不支持 |
-| radius | double? | - | 内容区圆角,默认主题大圆角。 |
+| height | double? | - | 高度;`TPopupPlacement.top`、`TPopupPlacement.bottom` 生效;`TPopupPlacement.center` 约束面板尺寸。 |
+| inset | TPopupTopInset? | - | 交叉轴边缘留白;具体类型由 `placement` 决定。 * `TPopupPlacement.bottom` 使用 `TPopupBottomInset` * `TPopupPlacement.top` 使用 `TPopupTopInset` * `TPopupPlacement.left` 使用 `TPopupLeftInset` * `TPopupPlacement.right` 使用 `TPopupRightInset` * `TPopupPlacement.center` 不支持 |
+
+#### 默认构造方法
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 |
 | backgroundColor | Color? | - | 内容区背景色,默认主题容器色。 |
-| showOverlay | bool | true | 是否绘制半透明蒙层。 当 [modal] 为 true 且此值为 false 时,为“透明模态弹层”。 |
+| cancelBuilder | TPopupSlotBuilder? | _kPopupDefaultCancel | bottom 左侧操作槽;仅 `headerBuilder` 为内置默认时生效。 内置默认为「取消」,点击触发 `TPopupTrigger.cancel`。 |
+| child | Widget | - | 浮层主体内容(必填)。 |
+| closeBuilder | TPopupSlotBuilder? | _kPopupDefaultClose | center 面板外下方关闭区;仅 `TPopupPlacement.center` 生效。三态见类文档「Builder 三态」。 内置默认点击触发 `TPopupTrigger.close`。 |
 | closeOnOverlayClick | bool? | - | - |
-| overlayColor | Color? | - | 蒙层颜色,默认 black54。 |
-| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 [overlayColor] 的 alpha 相乘后用于绘制。 |
-| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 [showOverlay] 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
+| confirmBuilder | TPopupSlotBuilder? | _kPopupDefaultConfirm | bottom 右侧操作槽;仅 `headerBuilder` 为内置默认时生效。 内置默认为「确定」,点击触发 `TPopupTrigger.confirm`。 |
 | destroyOnClose | bool | false | 为 true 时路由 `maintainState` 为 false,关闭后不保留路由内 State。 |
-| animationDuration | Duration | const Duration(milliseconds: 240) | 打开/关闭动画时长。 |
+| headerBuilder | TPopupHeaderBuilder? | _kPopupDefaultHeader | bottom 头部;仅 `TPopupPlacement.bottom` 生效。三态见类文档「Builder 三态」。 自定义时忽略 `titleWidget`、`cancelBuilder`、`confirmBuilder`。 |
+| height | double? | - | 高度;`TPopupPlacement.top`、`TPopupPlacement.bottom` 生效;`TPopupPlacement.center` 约束面板尺寸。 |
+| inset | TPopupInset? | - | 交叉轴边缘留白;具体类型由 `placement` 决定。 * `TPopupPlacement.bottom` 使用 `TPopupBottomInset` * `TPopupPlacement.top` 使用 `TPopupTopInset` * `TPopupPlacement.left` 使用 `TPopupLeftInset` * `TPopupPlacement.right` 使用 `TPopupRightInset` * `TPopupPlacement.center` 不支持 |
+| modal | bool | true | 是否以模态方式展示;为 true 时阻断背景交互与底层语义/焦点。 结合 `showOverlay` 可表达三种模式: * `modal=true, showOverlay=true`:标准模态弹层 * `modal=true, showOverlay=false`:透明模态弹层 * `modal=false, showOverlay=false`:非模态浮层 |
+| onClose | VoidCallback? | - | 开始关闭(与 `onVisibleChange` 的 `visible: false` 同期)。 |
+| onClosed | VoidCallback? | - | 当前展示周期真正结束。 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 |
 | onOpen | VoidCallback? | - | 路由 push 时(打开动画开始前)。 |
 | onOpened | VoidCallback? | - | 打开动画结束。 |
-| onClose | VoidCallback? | - | 开始关闭(与 [onVisibleChange] 的 `visible: false` 同期)。 |
-| onClosed | VoidCallback? | - | 当前展示周期真正结束。 大多数场景下会在关闭动画结束后触发;非栈顶路由被直接移除时不保证存在关闭动画。 |
-| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 [TPopupTrigger]。 |
-| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 [closeOnOverlayClick]。 |
+| onOverlayClick | VoidCallback? | - | 蒙层点击;是否关闭取决于 `closeOnOverlayClick`。 |
+| onVisibleChange | TPopupVisibleChangeCallback? | - | 显隐变化;第二个参数为 `TPopupTrigger`。 |
+| overlayColor | Color? | - | 蒙层颜色,默认 black54。 |
+| overlayOpacity | double? | - | 蒙层透明度系数(0–1),与 `overlayColor` 的 alpha 相乘后用于绘制。 |
+| placement | TPopupPlacement | TPopupPlacement.bottom | 出现位置,默认 `TPopupPlacement.bottom`。 |
+| radius | double? | - | 内容区圆角,默认主题大圆角。 |
+| showOverlay | bool | true | 是否绘制半透明蒙层。 当 `modal` 为 true 且此值为 false 时,为“透明模态弹层”。 |
+| titleWidget | Widget? | - | bottom 标题插槽;仅 `headerBuilder` 为内置默认时生效。`null` 表示无标题。 |
+| width | double? | - | 宽度;`TPopupPlacement.left`、`TPopupPlacement.right`、`TPopupPlacement.center` 生效。 |
 
 
 ### TPopupHandle
 #### 简介
-[TPopup.show] 的返回值,用于控制同一份 [TPopupOptions] 的多次打开与关闭。
-
- **示例**
-
- ```dart
- final handle = TPopup.show(
-   context,
-   options: TPopupOptions.bottom(child: panel),
- );
- handle.close();
- handle.open(); // 可省略 context,复用已缓存的 Navigator
- ```
+`TPopup.show` 的返回值,用于控制同一份 `TPopupOptions` 的多次打开与关闭。
 #### 公开属性
 
 | 属性 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
-| navigatorContext | BuildContext? | - | 与 [TPopup.show] 的 [navigatorContext] 相同。 |
-| options | TPopupOptions | - | 创建时传入的配置;每次 [open] 会按 [TPopupOptions.placement] 裁剪无效字段后使用。 |
-| useRootNavigator | bool | - | 与 [TPopup.show] 的 [useRootNavigator] 相同。 |
-
-
-#### 工厂构造方法
-
-##### TPopupHandle._
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| options | TPopupOptions | - | 创建时传入的配置;每次 [open] 会按 [TPopupOptions.placement] 裁剪无效字段后使用。 |
-| navigatorContext | BuildContext? | - | 与 [TPopup.show] 的 [navigatorContext] 相同。 |
-| useRootNavigator | bool | false | 与 [TPopup.show] 的 [useRootNavigator] 相同。 |
+| navigatorContext | BuildContext? | - | 与 `TPopup.show` 的 `navigatorContext` 相同。 |
+| options | TPopupOptions | - | 创建时传入的配置;每次 `open` 会按 `TPopupOptions.placement` 裁剪无效字段后使用。 |
+| useRootNavigator | bool | - | 与 `TPopup.show` 的 `useRootNavigator` 相同。 |
 
 
 ### TPopupPlacement
 #### 简介
-浮层出现方向;决定 [TPopupOptions] 中哪些字段生效。
-
- 与 [TPopupOptions] 类文档中的「字段与 placement」表对应。
- 方向固定时请用 [TPopupOptions.bottom]、[TPopupOptions.center] 等命名工厂。
+浮层出现方向;决定 `TPopupOptions` 中哪些字段生效。
+与 `TPopupOptions` 类文档中的「字段与 placement」表对应。
+方向固定时请用 `TPopupOptions.bottom`、`TPopupOptions.center` 等命名工厂。
 #### 枚举值
 
 
 | 名称 | 说明 |
 | --- | --- |
-| top | 自顶部滑入;使用 [TPopupOptions.height]、[TPopupOptions.inset]([TPopupTopInset])。 |
-| left | 自左侧滑入;使用 [TPopupOptions.width]、[TPopupOptions.inset]([TPopupLeftInset])。 |
-| right | 自右侧滑入;使用 [TPopupOptions.width]、[TPopupOptions.inset]([TPopupRightInset])。 |
-| bottom | 自底部滑入;默认内置头部;使用 [TPopupOptions.height]、[TPopupOptions.inset]([TPopupBottomInset])。 |
-| center | 屏幕居中;使用 [TPopupOptions.closeBuilder] 控制面板外下方关闭区。 |
+| top | 自顶部滑入;使用 `TPopupOptions.height`、`TPopupOptions.inset`(`TPopupTopInset`)。 |
+| left | 自左侧滑入;使用 `TPopupOptions.width`、`TPopupOptions.inset`(`TPopupLeftInset`)。 |
+| right | 自右侧滑入;使用 `TPopupOptions.width`、`TPopupOptions.inset`(`TPopupRightInset`)。 |
+| bottom | 自底部滑入;默认内置头部;使用 `TPopupOptions.height`、`TPopupOptions.inset`(`TPopupBottomInset`)。 |
+| center | 屏幕居中;使用 `TPopupOptions.closeBuilder` 控制面板外下方关闭区。 |
 
 
 ### TPopupTrigger
 #### 简介
 浮层关闭或显隐变化时的触发来源。
-
- 作为 [TPopupVisibleChangeCallback] 的第二个参数,以及关闭流程中的语义标记。
-
- 内置控件会映射为 [TPopupTrigger.overlay]、[TPopupTrigger.cancel]、
- [TPopupTrigger.confirm]、[TPopupTrigger.close];
- [TPopupHandle.close] 为 [TPopupTrigger.api];系统返回为
- [TPopupTrigger.systemBack];[headerBuilder] 内调用 `close` 等为
- [TPopupTrigger.custom]。
+作为 `TPopupVisibleChangeCallback` 的第二个参数,以及关闭流程中的语义标记。
+内置控件会映射为 `TPopupTrigger.overlay`、`TPopupTrigger.cancel`、
+`TPopupTrigger.confirm`、`TPopupTrigger.close`;
+`TPopupHandle.close` 为 `TPopupTrigger.api`;系统返回为
+`TPopupTrigger.systemBack`;`headerBuilder` 内调用 `close` 等为
+`TPopupTrigger.custom`。
 #### 枚举值
 
 
 | 名称 | 说明 |
 | --- | --- |
-| overlay | 点击蒙层,且 [TPopupOptions.closeOnOverlayClick] 为 true。 |
-| cancel | 点击 bottom 取消语义槽位(含默认与自定义 [TPopupOptions.cancelBuilder])。 |
-| confirm | 点击 bottom 确认语义槽位(含默认与自定义 [TPopupOptions.confirmBuilder])。 |
-| close | 点击 center 关闭语义槽位(含默认与自定义 [TPopupOptions.closeBuilder])。 |
-| api | 外部 API 主动触发的显隐变化,如 [TPopupHandle.close] 或打开事件。 |
+| overlay | 点击蒙层,且 `TPopupOptions.closeOnOverlayClick` 为 true。 |
+| cancel | 点击 bottom 取消语义槽位(含默认与自定义 `TPopupOptions.cancelBuilder`)。 |
+| confirm | 点击 bottom 确认语义槽位(含默认与自定义 `TPopupOptions.confirmBuilder`)。 |
+| close | 点击 center 关闭语义槽位(含默认与自定义 `TPopupOptions.closeBuilder`)。 |
+| api | 外部 API 主动触发的显隐变化,如 `TPopupHandle.close` 或打开事件。 |
 | systemBack | 系统返回键或系统路由返回触发的关闭。 |
-| custom | 无框架预设动作语义的自定义关闭,如 [headerBuilder] 内调用 `close`。 |
+| custom | 无框架预设动作语义的自定义关闭,如 `headerBuilder` 内调用 `close`。 |
 
 
   
\ No newline at end of file
diff --git a/tdesign-site/src/pull-down-refresh/README.md b/tdesign-site/src/pull-down-refresh/README.md
index 8bc3e0705..ba2157fc6 100644
--- a/tdesign-site/src/pull-down-refresh/README.md
+++ b/tdesign-site/src/pull-down-refresh/README.md
@@ -84,7 +84,7 @@ import 'package:easy_refresh/easy_refresh.dart';
 ### TRefreshHeader
 #### 简介
 TDesign刷新头部
- 结合EasyRefresh类实现下拉刷新,继承自Header类,字段含义与父类一致
+结合EasyRefresh类实现下拉刷新,继承自Header类,字段含义与父类一致
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -109,7 +109,7 @@ TDesign刷新头部
 | loadingIcon | TLoadingIcon | TLoadingIcon.circle | loading样式 |
 | maxOverOffset | - | - | - |
 | notifyWhenInvisible | - | - | - |
-| overScroll | bool | true | 越界滚动([enableInfiniteRefresh]为true或[infiniteOffset]有值时生效) |
+| overScroll | bool | true | 越界滚动(`enableInfiniteRefresh`为true或`infiniteOffset`有值时生效) |
 | position | - | - | - |
 | processedDuration | Duration? | - | - |
 | readySpringBuilder | - | - | - |
@@ -120,7 +120,7 @@ TDesign刷新头部
 | secondaryVelocity | - | - | - |
 | spring | - | - | - |
 | springRebound | - | - | - |
-| triggerDistance | double | 48.0 | 触发刷新任务的偏移量,同[triggerOffset] |
+| triggerDistance | double | 48.0 | 触发刷新任务的偏移量,同`triggerOffset` |
 | triggerOffset | double? | - | - |
 | triggerWhenReach | - | - | - |
 | triggerWhenRelease | - | - | - |
diff --git a/tdesign-site/src/rate/README.md b/tdesign-site/src/rate/README.md
index 5d53b6ba7..4631d4303 100644
--- a/tdesign-site/src/rate/README.md
+++ b/tdesign-site/src/rate/README.md
@@ -212,28 +212,30 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 ## API
 ### TRate
+#### 简介
+评分组件
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
 | allowHalf | bool? | false | 是否允许半选 |
-| builderText | Widget Function(BuildContext context, double value)? | - | 评分等级对应的辅助文字自定义构建,优先级高于[texts] 配置后,会忽略[texts],[textWidth],[iconTextGap] |
-| color | List? | - | 评分图标的颜色,示例:[选中颜色] / [选中颜色,未选中颜色],默认:[TTheme.of(context).warningColor5, TTheme.of(context).grayColor4] |
+| builderText | Widget Function(BuildContext context, double value)? | - | 评分等级对应的辅助文字自定义构建,优先级高于`texts` 配置后,会忽略`texts`,`textWidth`,`iconTextGap` |
+| color | List? | - | 评分图标的颜色,示例:`选中颜色` / `选中颜色,未选中颜色`,默认:`TTheme.of(context).warningColor5, TTheme.of(context).grayColor4` |
 | count | int? | 5 | 评分的数量 |
 | crossAxisAlignment | CrossAxisAlignment? | CrossAxisAlignment.center | 评分图标与辅助文字的交叉轴对齐方式 |
 | direction | Axis? | Axis.horizontal | 评分图标与辅助文字的布局方向 |
 | disabled | bool? | false | 是否禁用评分 |
 | gap | double? | - | 评分图标的间距,默认:TTheme.of(context).spacer8 |
-| icon | List? | - | 自定义评分图标,[选中和未选中图标] / [选中图标,未选中图标],默认:[TIcons.star_filled] |
-| iconTextGap | double? | - | 评分图标与辅助文字的间距,默认:[TTheme.of(context).spacer16] |
+| icon | List? | - | 自定义评分图标,`选中和未选中图标` / `选中图标,未选中图标`,默认:`TIcons.star_filled` |
+| iconTextGap | double? | - | 评分图标与辅助文字的间距,默认:`TTheme.of(context).spacer16` |
 | key | Key? | - | 组件标识,用于区分或保留组件状态。 |
 | mainAxisAlignment | MainAxisAlignment? | MainAxisAlignment.start | 评分图标与辅助文字的主轴对齐方式 |
 | mainAxisSize | MainAxisSize? | MainAxisSize.min | 评分图标与辅助文字主轴方向上如何占用空间 |
 | onChange | void Function(double value)? | - | 评分数改变时触发 |
-| placement | PlacementEnum? | PlacementEnum.top | 选择评分弹框的位置,值为[PlacementEnum.none]表示不显示评分弹框。 |
+| placement | PlacementEnum? | PlacementEnum.top | 选择评分弹框的位置,值为`PlacementEnum.none`表示不显示评分弹框。 |
 | showText | bool? | false | 是否显示对应的辅助文字 |
 | size | double? | 24.0 | 评分图标的大小 |
-| texts | List? | const ['极差', '失望', '一般', '满意', '惊喜'] | 评分等级对应的辅助文字, 当[allowHalf]为false时长度应与[count]一致, 当[allowHalf]为true时长度应为[count]的两倍, 自定义值示例:['1分', '2分', '3分', '4分', '5分']。 |
+| texts | List? | const ['极差', '失望', '一般', '满意', '惊喜'] | 评分等级对应的辅助文字, 当`allowHalf`为false时长度应与`count`一致, 当`allowHalf`为true时长度应为`count`的两倍, 自定义值示例:`'1分', '2分', '3分', '4分', '5分'`。 |
 | textWidth | double? | 48.0 | 评分等级对应的辅助文字宽度 |
 | value | double? | 0 | 选择评分的值 |
 
diff --git a/tdesign-site/src/swipe-cell/README.md b/tdesign-site/src/swipe-cell/README.md
index 0947a8d2e..dc4da7579 100644
--- a/tdesign-site/src/swipe-cell/README.md
+++ b/tdesign-site/src/swipe-cell/README.md
@@ -340,34 +340,13 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 ### TSwipeCell
 #### 简介
 滑动单元格组件
-#### 默认构造方法
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| cell | Widget | - | 单元格 [TCell] |
-| closeWhenOpened | bool? | true | 当同一组([groupTag])中的一个[TSwipeCell]打开时,是否关闭组中的所有其他[TSwipeCell] |
-| closeWhenTapped | bool? | true | 当同一组([groupTag])中的一个[TSwipeCell]被点击时,是否应该关闭组中的所有[TSwipeCell] [cell]组件被点击时必须传递点击事件,执行`TSwipeCellInherited.of(context)?.cellClick()` |
-| controller | SlidableController? | - | 自定义控制滑动窗口 |
-| direction | Axis? | Axis.horizontal | 可拖动的方向 |
-| disabled | bool? | false | 是否禁用滑动 |
-| dragStartBehavior | DragStartBehavior? | DragStartBehavior.start | 处理拖动开始行为的方式[GestureDetector.dragStartBehavior] |
-| duration | Duration? | const Duration(milliseconds: 200) | 打开关闭动画时长 |
-| groupTag | Object? | - | 组,配置后,[closeWhenOpened]、[closeWhenTapped]才起作用 |
-| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
-| left | TSwipeCellPanel? | - | 左侧滑动操作项面板 |
-| onChange | Function(TSwipeDirection direction, bool open)? | - | 滑动展开事件 |
-| opened | List? | const [false, false] | 默认打开,[left, right] |
-| right | TSwipeCellPanel? | - | 右侧滑动操作项面板 |
-| slidableKey | Key? | - | 滑动组件的 Key |
-
 
 #### 静态方法
 
 ##### TSwipeCell.close
 
-根据[groupTag]关闭[TSwipeCell]
-
- current:保留当前不关闭
+根据`groupTag`关闭`TSwipeCell`
+current:保留当前不关闭
 
 返回类型:`void`
 
@@ -379,7 +358,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 
 ##### TSwipeCell.of
 
-获取上下文最近的[controller]
+获取上下文最近的`controller`
 
 返回类型:`SlidableController?`
 
@@ -387,6 +366,26 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 | --- | --- | --- | --- |
 | context | BuildContext | - | - |
 
+#### 默认构造方法
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| cell | Widget | - | 单元格 `TCell` |
+| closeWhenOpened | bool? | true | 当同一组(`groupTag`)中的一个`TSwipeCell`打开时,是否关闭组中的所有其他`TSwipeCell` |
+| closeWhenTapped | bool? | true | 当同一组(`groupTag`)中的一个`TSwipeCell`被点击时,是否应该关闭组中的所有`TSwipeCell` `cell`组件被点击时必须传递点击事件,执行`TSwipeCellInherited.of(context)?.cellClick()` |
+| controller | SlidableController? | - | 自定义控制滑动窗口 |
+| direction | Axis? | Axis.horizontal | 可拖动的方向 |
+| disabled | bool? | false | 是否禁用滑动 |
+| dragStartBehavior | DragStartBehavior? | DragStartBehavior.start | 处理拖动开始行为的方式`GestureDetector.dragStartBehavior` |
+| duration | Duration? | const Duration(milliseconds: 200) | 打开关闭动画时长 |
+| groupTag | Object? | - | 组,配置后,`closeWhenOpened`、`closeWhenTapped`才起作用 |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
+| left | TSwipeCellPanel? | - | 左侧滑动操作项面板 |
+| onChange | Function(TSwipeDirection direction, bool open)? | - | 滑动展开事件 |
+| opened | List? | const [false, false] | 默认打开,`left, right` |
+| right | TSwipeCellPanel? | - | 右侧滑动操作项面板 |
+| slidableKey | Key? | - | 滑动组件的 Key |
+
 
 ### TSwipeDirection
 #### 枚举值
diff --git a/tdesign-site/src/swiper/README.md b/tdesign-site/src/swiper/README.md
index 1d0d2ed98..bb1019d88 100644
--- a/tdesign-site/src/swiper/README.md
+++ b/tdesign-site/src/swiper/README.md
@@ -292,14 +292,6 @@ TDesign风格的Swiper指示器样式,与flutter_swiper的Swiper结合使用
 ### TPageTransformer
 #### 简介
 TD默认PageTransformer
-#### 默认构造方法
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| fade | double? | - | 淡化比例 |
-| margin | double? | - | 左右间隔 |
-| scale | double? | - | 缩放比例 |
-
 
 #### 工厂构造方法
 
@@ -321,5 +313,13 @@ TD默认PageTransformer
 | fade | double? | 1 | 淡化比例 |
 | scale | double? | 0.8 | 缩放比例 |
 
+#### 默认构造方法
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| fade | double? | - | 淡化比例 |
+| margin | double? | - | 左右间隔 |
+| scale | double? | - | 缩放比例 |
+
 
   
\ No newline at end of file
diff --git a/tdesign-site/src/tag/README.md b/tdesign-site/src/tag/README.md
index 056f0bab4..8201d455c 100644
--- a/tdesign-site/src/tag/README.md
+++ b/tdesign-site/src/tag/README.md
@@ -503,6 +503,9 @@ Mark标签
 
 ## API
 ### TTag
+#### 简介
+展示型标签组件,仅展示,内部不可更改自身状态
+支持样式:方形/圆角/半圆/带关闭图标
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -531,6 +534,9 @@ Mark标签
 
 
 ### TSelectTag
+#### 简介
+点击型标签组件,点击时内部更改自身状态
+支持样式:方形/圆角/半圆/带关闭图标
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -558,25 +564,8 @@ Mark标签
 
 
 ### TTagStyle
-#### 默认构造方法
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| backgroundColor | Color? | - | 背景颜色 |
-| border | double | 0 | 线框粗细 |
-| borderColor | Color? | - | 边框颜色 |
-| borderRadius | BorderRadiusGeometry? | - | 圆角 |
-| context | BuildContext? | - | 上下文,方便获取主题内容 |
-| font | Font? | - | 字体尺寸 |
-| fontWeight | FontWeight? | - | 字体粗细 |
-| textColor | Color? | - | 文字颜色 |
-
-#### 公开属性
-
-| 属性 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| closeIconColor | Color? | - | 关闭图标颜色 |
-
+#### 简介
+标签样式
 
 #### 工厂构造方法
 
@@ -615,6 +604,25 @@ Mark标签
 | light | bool | - | - |
 | shape | TTagShape | - | - |
 
+#### 默认构造方法
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| backgroundColor | Color? | - | 背景颜色 |
+| border | double | 0 | 线框粗细 |
+| borderColor | Color? | - | 边框颜色 |
+| borderRadius | BorderRadiusGeometry? | - | 圆角 |
+| context | BuildContext? | - | 上下文,方便获取主题内容 |
+| font | Font? | - | 字体尺寸 |
+| fontWeight | FontWeight? | - | 字体粗细 |
+| textColor | Color? | - | 文字颜色 |
+
+#### 公开属性
+
+| 属性 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| closeIconColor | Color? | - | 关闭图标颜色 |
+
 
 ### TTagTheme
 #### 简介
diff --git a/tdesign-site/src/text/README.md b/tdesign-site/src/text/README.md
index a72595af3..0a9495aa9 100644
--- a/tdesign-site/src/text/README.md
+++ b/tdesign-site/src/text/README.md
@@ -195,42 +195,27 @@ TText.rich测试:
 
 ## API
 ### TText
-#### 默认构造方法
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| data | - | - | 以下系统 text 属性,释义请参考系统 [Text] 中注释 |
-| backgroundColor | Color? | - | 背景颜色 |
-| font | Font? | - | 字体尺寸,包含 大小size 和 行高height |
-| fontFamily | FontFamily? | - | 字体ttf |
-| fontFamilyUrl | String? | - | 是否禁用懒加载 FontFamily 的能力 |
-| fontWeight | FontWeight? | - | 字体粗细 |
-| forceVerticalCenter | bool | false | 是否强制居中 |
-| isInFontLoader | bool | false | 是否在 FontLoader 中使用 |
-| isTextThrough | bool? | false | 是否是横线穿过样式(删除线) |
-| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
-| lineThroughColor | Color? | - | 删除线颜色,对应 TestStyle 的 decorationColor |
-| locale | Locale? | - | - |
-| maxLines | int? | - | - |
-| overflow | TextOverflow? | - | - |
-| package | String? | - | 字体包名 |
-| semanticsLabel | String? | - | - |
-| softWrap | bool? | - | - |
-| strutStyle | StrutStyle? | - | - |
-| style | TextStyle? | - | 自定义的 TextStyle,其中指定的属性,将覆盖扩展的外层属性 |
-| textAlign | TextAlign? | - | - |
-| textColor | Color? | - | 文本颜色 |
-| textDirection | TextDirection? | - | - |
-| textHeightBehavior | ui.TextHeightBehavior? | - | - |
-| textScaleFactor | double? | - | - |
-| textWidthBasis | TextWidthBasis? | - | - |
-
-#### 公开属性
-
-| 属性 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| textSpan | InlineSpan? | - | - |
-
+#### 简介
+文本控件
+设计原则:
+1. 为了使用更方便,所以对系统组件进行的扩展,需兼容系统控件所有功能,不能让用户使用 TDesign 时,因不能满足系统功能而弃用。
+2. 非系统已有属性,尽量添加注释
+需求:把一部分在 TextStyle 中的属性扁平化,放到外层。
+1. 暴露系统的所有属性,支持系统所有操作
+2. 约束使用主题配置的几种字体
+3. 提供转换为系统 Text 的方法,以使某些系统组件指定接收系统 Text 时可使用。(Image 组件同理)
+4. 支持自定义 TextStyle
+5. 兼容 TextSpan 形式
+技巧:
+命名参数替换属性的正则:
+第一步,把 Text 中的可选参数拷贝过来,变成如下格式:
+Text(data,
+this.style,
+this.strutStyle,
+……)
+第二步,正则替换如下:
+匹配(前半有默认值,后半无默认值):this\.(`a-z|A-Z`+)[]*`\=`+[]*`a-z|A-Z`+\,|this\.(`a-z|A-Z`+)\,
+替换:$1$2: this.$1$2,
 
 #### 工厂构造方法
 
@@ -266,8 +251,46 @@ TText.rich测试:
 | isInFontLoader | bool | false | 是否在 FontLoader 中使用 |
 | fontFamilyUrl | String? | - | 是否禁用懒加载 FontFamily 的能力 |
 
+#### 默认构造方法
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| data | - | - | 以下系统 text 属性,释义请参考系统 `Text` 中注释 |
+| backgroundColor | Color? | - | 背景颜色 |
+| font | Font? | - | 字体尺寸,包含 大小size 和 行高height |
+| fontFamily | FontFamily? | - | 字体ttf |
+| fontFamilyUrl | String? | - | 是否禁用懒加载 FontFamily 的能力 |
+| fontWeight | FontWeight? | - | 字体粗细 |
+| forceVerticalCenter | bool | false | 是否强制居中 |
+| isInFontLoader | bool | false | 是否在 FontLoader 中使用 |
+| isTextThrough | bool? | false | 是否是横线穿过样式(删除线) |
+| key | Key? | - | 组件标识,用于区分或保留组件状态。 |
+| lineThroughColor | Color? | - | 删除线颜色,对应 TestStyle 的 decorationColor |
+| locale | Locale? | - | - |
+| maxLines | int? | - | - |
+| overflow | TextOverflow? | - | - |
+| package | String? | - | 字体包名 |
+| semanticsLabel | String? | - | - |
+| softWrap | bool? | - | - |
+| strutStyle | StrutStyle? | - | - |
+| style | TextStyle? | - | 自定义的 TextStyle,其中指定的属性,将覆盖扩展的外层属性 |
+| textAlign | TextAlign? | - | - |
+| textColor | Color? | - | 文本颜色 |
+| textDirection | TextDirection? | - | - |
+| textHeightBehavior | ui.TextHeightBehavior? | - | - |
+| textScaleFactor | double? | - | - |
+| textWidthBasis | TextWidthBasis? | - | - |
+
+#### 公开属性
+
+| 属性 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| textSpan | InlineSpan? | - | - |
+
 
 ### TTextSpan
+#### 简介
+TextSpan 的 TDesign 扩展,将部分 TextStyle 中的参数扁平化。
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -291,6 +314,8 @@ TText.rich测试:
 
 
 ### TTextConfiguration
+#### 简介
+存储可以自定义 TText 居中算法数据的内部控件
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
diff --git a/tdesign-site/src/time-counter/README.md b/tdesign-site/src/time-counter/README.md
index d0eb22c69..f00d51b28 100644
--- a/tdesign-site/src/time-counter/README.md
+++ b/tdesign-site/src/time-counter/README.md
@@ -638,6 +638,20 @@ TTimeCounter _buildCustomUnitLargeSize(BuildContext context) {
 ### TTimeCounterStyle
 #### 简介
 计时组件样式
+
+#### 工厂构造方法
+
+##### TTimeCounterStyle.generateStyle
+
+生成默认样式
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| context | BuildContext | - | - |
+| size | TTimeCounterSize? | - | - |
+| theme | TTimeCounterTheme? | - | - |
+| splitWithUnit | bool? | - | - |
+
 #### 默认构造方法
 
 | 参数 | 类型 | 默认值 | 说明 |
@@ -659,20 +673,6 @@ TTimeCounter _buildCustomUnitLargeSize(BuildContext context) {
 | timeWidth | double? | - | 时间容器宽度 |
 
 
-#### 工厂构造方法
-
-##### TTimeCounterStyle.generateStyle
-
-生成默认样式
-
-| 参数 | 类型 | 默认值 | 说明 |
-| --- | --- | --- | --- |
-| context | BuildContext | - | - |
-| size | TTimeCounterSize? | - | - |
-| theme | TTimeCounterTheme? | - | - |
-| splitWithUnit | bool? | - | - |
-
-
 ### TTimeCounterStatus
 #### 简介
 计时组件控制器转态