06dc68245a
- 新增 showDateRangeDropdown:锚定触发控件下方(透明遮罩点外关闭,形态同 DsMenu 下拉),起始/结束两列并排,各带可输入框(键入/回车/失焦归一 yyyy-MM-dd)与滚轮,输入↔滚轮双向联动;共用底栏显示当前范围 + 确定, 起止倒置自动交换;窄屏回落既有两步滚轮弹窗 - WheelDatePanel 加 showFooter/onChanged(滚动即回调),供范围面板复用 - 出入库列表 6 处调用切换:工具栏时间 chip「自定义…」、桌面详搜弹窗、 窄屏详搜 sheet 的日期区间字段均锚定在各自控件下方 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
622 lines
21 KiB
Dart
622 lines
21 KiB
Dart
import 'dart:math' as math;
|
||
|
||
import 'package:flutter/material.dart';
|
||
|
||
import '../core/responsive/responsive.dart';
|
||
import '../core/theme/context_tokens.dart';
|
||
import '../core/theme/app_tokens.dart';
|
||
import '../core/theme/app_dims.g.dart';
|
||
import '../core/utils/date_util.dart';
|
||
import '../core/utils/dialog_util.dart';
|
||
import '../core/theme/app_fonts.dart';
|
||
|
||
/// 年/月/日 滚轮日期选择面板(对齐原型单一真源 .wheel-pop / datewheel.js)。
|
||
/// 三列纯数字(年 4 位、月/日补零两位,等宽字体),中间高亮带 = .wheel-center,
|
||
/// 居中项为 primary 加粗 16px;底部「今天」+「确定」,无取消(点外部取消)。
|
||
///
|
||
/// 本体是自包含的 248px 卡片(Material 描边+阴影),既可放进锚定浮层(单据/生产日期,
|
||
/// 挂在字段下方,对齐原型 datewheel.js 的 pop 定位),也可放进居中弹窗(范围选择)。
|
||
/// 「确定」时回调 [onCommit];「今天」仅在面板内滚动到今天,不提交。
|
||
class WheelDatePanel extends StatefulWidget {
|
||
final DateTime initial;
|
||
final ValueChanged<DateTime> onCommit;
|
||
|
||
/// 可选标题(范围选择用「起始/结束日期」区分);空则不渲染(对齐原型无标题)。
|
||
final String title;
|
||
|
||
/// 滚动即回调当前值(范围下拉的联动输入框用);null 不回调。
|
||
final ValueChanged<DateTime>? onChanged;
|
||
|
||
/// false 隐藏「今天/确定」底栏(范围下拉共用一个底栏时用),onCommit 不再触发。
|
||
final bool showFooter;
|
||
|
||
const WheelDatePanel({
|
||
super.key,
|
||
required this.initial,
|
||
required this.onCommit,
|
||
this.title = '',
|
||
this.onChanged,
|
||
this.showFooter = true,
|
||
});
|
||
|
||
@override
|
||
State<WheelDatePanel> createState() => _WheelDatePanelState();
|
||
}
|
||
|
||
const double _kItem = 36; // .wheel-item 行高
|
||
const double _kColsH = 180; // .wheel-cols 高度(5 行)
|
||
const double _kWidth = 248; // .wheel-pop 宽度
|
||
|
||
String _pad2(int n) => n.toString().padLeft(2, '0');
|
||
int _daysIn(int y, int m) => DateTime(y, m + 1, 0).day; // m: 1-12
|
||
|
||
class _WheelDatePanelState extends State<WheelDatePanel> {
|
||
late final List<int> _years; // now-15 .. now+2(对齐 datewheel.js)
|
||
late int _yIdx, _mIdx, _dIdx;
|
||
late final FixedExtentScrollController _yCtrl, _mCtrl, _dCtrl;
|
||
|
||
int get _year => _years[_yIdx];
|
||
int get _month => _mIdx + 1;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
final now = DateTime.now();
|
||
_years = [for (var y = now.year - 15; y <= now.year + 2; y++) y];
|
||
final v = widget.initial;
|
||
_yIdx = _years.indexOf(v.year);
|
||
if (_yIdx < 0) _yIdx = _years.indexOf(now.year);
|
||
if (_yIdx < 0) _yIdx = 0;
|
||
_mIdx = (v.month - 1).clamp(0, 11);
|
||
_dIdx = (v.day - 1).clamp(0, _daysIn(v.year, v.month) - 1);
|
||
_yCtrl = FixedExtentScrollController(initialItem: _yIdx);
|
||
_mCtrl = FixedExtentScrollController(initialItem: _mIdx);
|
||
_dCtrl = FixedExtentScrollController(initialItem: _dIdx);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_yCtrl.dispose();
|
||
_mCtrl.dispose();
|
||
_dCtrl.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
// 年/月变化后重算当月天数,超界则回夹并吸附。
|
||
void _onYearOrMonth() {
|
||
final n = _daysIn(_year, _month);
|
||
if (_dIdx > n - 1) {
|
||
_dIdx = n - 1;
|
||
_dCtrl.jumpToItem(_dIdx);
|
||
}
|
||
setState(() {});
|
||
widget.onChanged?.call(DateTime(_year, _month, _dIdx + 1));
|
||
}
|
||
|
||
void _goToday() {
|
||
final now = DateTime.now();
|
||
final yi = _years.indexOf(now.year);
|
||
if (yi >= 0) _yIdx = yi;
|
||
_mIdx = now.month - 1;
|
||
_dIdx = now.day - 1;
|
||
const d = Duration(milliseconds: 250);
|
||
_yCtrl.animateToItem(_yIdx, duration: d, curve: Curves.easeOut);
|
||
_mCtrl.animateToItem(_mIdx, duration: d, curve: Curves.easeOut);
|
||
_dCtrl.animateToItem(_dIdx, duration: d, curve: Curves.easeOut);
|
||
setState(() {});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
final days = _daysIn(_year, _month);
|
||
return Material(
|
||
color: t.surface,
|
||
elevation: 6,
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
child: Container(
|
||
width: _kWidth,
|
||
padding: const EdgeInsets.all(8),
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: t.border),
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
if (widget.title.isNotEmpty)
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(4, 2, 4, 8),
|
||
child: Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Text(widget.title,
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsTitle,
|
||
fontWeight: FontWeight.w600,
|
||
color: t.heading)),
|
||
),
|
||
),
|
||
// 三列滚轮 + 中间高亮带
|
||
SizedBox(
|
||
height: _kColsH,
|
||
child: Stack(
|
||
children: [
|
||
Positioned(
|
||
left: 4,
|
||
right: 4,
|
||
top: (_kColsH - _kItem) / 2,
|
||
height: _kItem,
|
||
child: IgnorePointer(
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
color: t.bg,
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
border: Border(
|
||
top: BorderSide(color: t.border),
|
||
bottom: BorderSide(color: t.border),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
Row(
|
||
children: [
|
||
_col(t, _yCtrl, _years.length,
|
||
(i) => _years[i].toString(), _yIdx, (i) {
|
||
_yIdx = i;
|
||
_onYearOrMonth();
|
||
}),
|
||
_col(t, _mCtrl, 12, (i) => _pad2(i + 1), _mIdx, (i) {
|
||
_mIdx = i;
|
||
_onYearOrMonth();
|
||
}),
|
||
_col(t, _dCtrl, days, (i) => _pad2(i + 1), _dIdx, (i) {
|
||
setState(() => _dIdx = i);
|
||
widget.onChanged
|
||
?.call(DateTime(_year, _month, _dIdx + 1));
|
||
}),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
// 底栏:今天(左) / 确定(右,无取消,点外部取消)
|
||
if (widget.showFooter)
|
||
Container(
|
||
margin: const EdgeInsets.only(top: 6),
|
||
padding: const EdgeInsets.fromLTRB(4, 8, 4, 2),
|
||
decoration: BoxDecoration(
|
||
border: Border(top: BorderSide(color: t.borderSubtle)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
InkWell(
|
||
onTap: _goToday,
|
||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 10, vertical: 4),
|
||
child: Text('今天',
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsSm, color: t.muted)),
|
||
),
|
||
),
|
||
const Spacer(),
|
||
InkWell(
|
||
onTap: () =>
|
||
widget.onCommit(DateTime(_year, _month, _dIdx + 1)),
|
||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 16, vertical: 5),
|
||
decoration: BoxDecoration(
|
||
color: t.primary,
|
||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||
),
|
||
child: Text('确定',
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsSm,
|
||
color: t.onPrimary,
|
||
fontWeight: FontWeight.w600)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _col(
|
||
AppTokens t,
|
||
FixedExtentScrollController ctrl,
|
||
int count,
|
||
String Function(int) label,
|
||
int selIdx,
|
||
ValueChanged<int> onSel,
|
||
) {
|
||
return Expanded(
|
||
child: ListWheelScrollView.useDelegate(
|
||
controller: ctrl,
|
||
itemExtent: _kItem,
|
||
physics: const FixedExtentScrollPhysics(),
|
||
diameterRatio: 100, // 近似平面,去掉滚轮弧度
|
||
perspective: 0.0001,
|
||
onSelectedItemChanged: onSel,
|
||
childDelegate: ListWheelChildBuilderDelegate(
|
||
childCount: count,
|
||
builder: (c, i) {
|
||
final on = i == selIdx;
|
||
return Center(
|
||
child: Text(
|
||
label(i),
|
||
style: TextStyle(
|
||
fontFamily: AppFonts.mono,
|
||
fontFamilyFallback: AppFonts.monoFallback,
|
||
fontSize: on ? 16 : AppDims.fsBody,
|
||
fontWeight: on ? FontWeight.w700 : FontWeight.w400,
|
||
color: on ? t.primary : t.muted,
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 居中弹窗形式(范围选择用)。锚定字段的用法见 DsDateCell(直接嵌 WheelDatePanel 到浮层)。
|
||
Future<DateTime?> showWheelDatePicker(
|
||
BuildContext context, {
|
||
DateTime? initial,
|
||
String title = '',
|
||
}) {
|
||
return showAppDialog<DateTime>(
|
||
context: context,
|
||
builder: (dctx) => Align(
|
||
alignment: Alignment.center,
|
||
child: WheelDatePanel(
|
||
initial: initial ?? DateTime.now(),
|
||
title: title,
|
||
onCommit: (d) => Navigator.of(dctx).pop(d),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 范围选择:先选起始日、再选结束日;任一取消则返回 null。
|
||
Future<DateTimeRange?> showWheelDateRange(
|
||
BuildContext context, {
|
||
DateTimeRange? initial,
|
||
}) async {
|
||
final start = await showWheelDatePicker(context,
|
||
initial: initial?.start, title: '起始日期');
|
||
if (start == null || !context.mounted) return null;
|
||
final end = await showWheelDatePicker(context,
|
||
initial: initial?.end ?? start, title: '结束日期');
|
||
if (end == null) return null;
|
||
return end.isBefore(start)
|
||
? DateTimeRange(start: end, end: start)
|
||
: DateTimeRange(start: start, end: end);
|
||
}
|
||
|
||
// ═══════════════ 日期范围下拉(桌面)═══════════════
|
||
// 锚定触发控件下方的双列面板:起始 | 结束 并排各一组「可输入框 + 滚轮」,
|
||
// 透明遮罩点外关闭(不冻结窗口,形态同 DsMenu 下拉);共用一个「确定」底栏。
|
||
// 窄屏(无悬停、面板超宽)回落到居中滚轮弹窗(showWheelDateRange)。
|
||
|
||
/// 打开日期范围下拉,锚定在 [anchorContext](触发控件自身的 context)下方。
|
||
/// 确定返回 DateTimeRange(起止倒置自动交换);点外部关闭返回 null(不变更)。
|
||
Future<DateTimeRange?> showDateRangeDropdown(
|
||
BuildContext anchorContext, {
|
||
DateTimeRange? initial,
|
||
}) {
|
||
if (anchorContext.isMobile) {
|
||
// 窄屏:双列 530px 放不下,保持既有两步滚轮弹窗
|
||
return showWheelDateRange(anchorContext, initial: initial);
|
||
}
|
||
final overlay = Navigator.of(anchorContext)
|
||
.overlay!
|
||
.context
|
||
.findRenderObject() as RenderBox;
|
||
final box = anchorContext.findRenderObject() as RenderBox?;
|
||
final rect = box == null
|
||
? Rect.zero
|
||
: box.localToGlobal(Offset.zero, ancestor: overlay) & box.size;
|
||
return Navigator.of(anchorContext).push<DateTimeRange>(
|
||
_RangeDropdownRoute(anchorRect: rect, initial: initial),
|
||
);
|
||
}
|
||
|
||
class _RangeDropdownRoute extends PopupRoute<DateTimeRange> {
|
||
final Rect anchorRect;
|
||
final DateTimeRange? initial;
|
||
_RangeDropdownRoute({required this.anchorRect, this.initial});
|
||
|
||
@override
|
||
Color? get barrierColor => Colors.transparent;
|
||
@override
|
||
bool get barrierDismissible => true;
|
||
@override
|
||
String? get barrierLabel => 'date-range';
|
||
@override
|
||
Duration get transitionDuration => Duration.zero;
|
||
|
||
@override
|
||
Widget buildPage(BuildContext context, Animation<double> animation,
|
||
Animation<double> secondaryAnimation) {
|
||
return CustomSingleChildLayout(
|
||
delegate: _RangeDropdownLayout(anchorRect),
|
||
child: _DateRangePanel(
|
||
initial: initial,
|
||
onCommit: (r) => Navigator.of(context).pop(r),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _RangeDropdownLayout extends SingleChildLayoutDelegate {
|
||
final Rect anchor;
|
||
_RangeDropdownLayout(this.anchor);
|
||
|
||
@override
|
||
BoxConstraints getConstraintsForChild(BoxConstraints constraints) =>
|
||
BoxConstraints(
|
||
maxWidth: math.max(0, constraints.maxWidth - 16),
|
||
maxHeight: math.max(0, constraints.maxHeight - 16),
|
||
);
|
||
|
||
@override
|
||
Offset getPositionForChild(Size size, Size childSize) {
|
||
final left = anchor.left
|
||
.clamp(8.0, math.max(8.0, size.width - childSize.width - 8))
|
||
.toDouble();
|
||
final below = anchor.bottom + 6;
|
||
final above = anchor.top - childSize.height - 6;
|
||
final top = (below + childSize.height <= size.height - 8 || above < 8)
|
||
? below
|
||
: above;
|
||
return Offset(left, top.clamp(8.0, math.max(8.0, size.height - 8)));
|
||
}
|
||
|
||
@override
|
||
bool shouldRelayout(_RangeDropdownLayout old) => old.anchor != anchor;
|
||
}
|
||
|
||
class _DateRangePanel extends StatefulWidget {
|
||
final DateTimeRange? initial;
|
||
final ValueChanged<DateTimeRange> onCommit;
|
||
const _DateRangePanel({this.initial, required this.onCommit});
|
||
|
||
@override
|
||
State<_DateRangePanel> createState() => _DateRangePanelState();
|
||
}
|
||
|
||
class _DateRangePanelState extends State<_DateRangePanel> {
|
||
late DateTime _start = widget.initial?.start ?? DateTime.now();
|
||
late DateTime _end = widget.initial?.end ?? DateTime.now();
|
||
|
||
void _commit() {
|
||
final r = _end.isBefore(_start)
|
||
? DateTimeRange(start: _end, end: _start)
|
||
: DateTimeRange(start: _start, end: _end);
|
||
widget.onCommit(r);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
return Material(
|
||
color: t.surface,
|
||
elevation: 10,
|
||
borderRadius: BorderRadius.circular(AppDims.rLg),
|
||
child: Container(
|
||
width: 552,
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: t.border),
|
||
borderRadius: BorderRadius.circular(AppDims.rLg),
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Expanded(
|
||
child: _RangeSide(
|
||
title: '起始日期',
|
||
value: _start,
|
||
onChanged: (d) => setState(() => _start = d),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: _RangeSide(
|
||
title: '结束日期',
|
||
value: _end,
|
||
onChanged: (d) => setState(() => _end = d),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
// 底栏(共用):范围文案(左)/ 确定(右);点外部即取消
|
||
Container(
|
||
margin: const EdgeInsets.only(top: 10),
|
||
padding: const EdgeInsets.only(top: 10),
|
||
decoration: BoxDecoration(
|
||
border: Border(top: BorderSide(color: t.borderSubtle)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
'${formatYmd(_start)} ~ ${formatYmd(_end)}',
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsSm,
|
||
color: t.muted,
|
||
fontFamily: AppFonts.mono,
|
||
fontFamilyFallback: AppFonts.monoFallback),
|
||
),
|
||
),
|
||
InkWell(
|
||
onTap: _commit,
|
||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 16, vertical: 5),
|
||
decoration: BoxDecoration(
|
||
color: t.primary,
|
||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||
),
|
||
child: Text('确定',
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsSm,
|
||
color: t.onPrimary,
|
||
fontWeight: FontWeight.w600)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 范围面板单侧:标题 + 可输入框(键入即联动滚轮)+ 滚轮(滚动即联动输入框)。
|
||
class _RangeSide extends StatefulWidget {
|
||
final String title;
|
||
final DateTime value;
|
||
final ValueChanged<DateTime> onChanged;
|
||
const _RangeSide(
|
||
{required this.title, required this.value, required this.onChanged});
|
||
|
||
@override
|
||
State<_RangeSide> createState() => _RangeSideState();
|
||
}
|
||
|
||
class _RangeSideState extends State<_RangeSide> {
|
||
late final TextEditingController _ctrl =
|
||
TextEditingController(text: formatYmd(widget.value));
|
||
final FocusNode _focus = FocusNode();
|
||
// 键入归一后的滚轮锚定值:仅键入产生完整有效日期时更新(驱动滚轮重建吸附)
|
||
late DateTime _wheelAnchor = widget.value;
|
||
bool _syncing = false; // 滚轮回写输入框时跳过 onChanged 循环
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_focus.addListener(() {
|
||
if (!_focus.hasFocus) {
|
||
// 失焦归一显示(2024 → 2024-01-01)并吸附滚轮
|
||
final n = normalizeYmd(_ctrl.text);
|
||
if (n != null) {
|
||
_syncing = true;
|
||
_ctrl.text = n;
|
||
_syncing = false;
|
||
_applyTyped(n);
|
||
} else {
|
||
_syncing = true;
|
||
_ctrl.text = formatYmd(widget.value);
|
||
_syncing = false;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_ctrl.dispose();
|
||
_focus.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _applyTyped(String ymd) {
|
||
final d = parseYmd(ymd);
|
||
if (d == null) return;
|
||
widget.onChanged(d);
|
||
if (d != _wheelAnchor) setState(() => _wheelAnchor = d);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.only(left: 2, bottom: 6),
|
||
child: Text(widget.title,
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsSm,
|
||
fontWeight: FontWeight.w600,
|
||
color: t.muted)),
|
||
),
|
||
Container(
|
||
height: 32,
|
||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||
margin: const EdgeInsets.only(bottom: 6),
|
||
decoration: BoxDecoration(
|
||
color: t.surface,
|
||
border: Border.all(color: _focus.hasFocus ? t.primary : t.border),
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
),
|
||
alignment: Alignment.centerLeft,
|
||
child: TextField(
|
||
controller: _ctrl,
|
||
focusNode: _focus,
|
||
keyboardType: TextInputType.datetime,
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsBody,
|
||
color: t.text,
|
||
fontFamily: AppFonts.mono,
|
||
fontFamilyFallback: AppFonts.monoFallback),
|
||
decoration: InputDecoration(
|
||
isCollapsed: true,
|
||
border: InputBorder.none,
|
||
enabledBorder: InputBorder.none,
|
||
focusedBorder: InputBorder.none,
|
||
filled: false,
|
||
hintText: '年-月-日',
|
||
hintStyle: TextStyle(fontSize: AppDims.fsBody, color: t.faint),
|
||
),
|
||
onChanged: (v) {
|
||
if (_syncing) return;
|
||
// 键入完整日期(含 8 位纯数字)即联动滚轮;不完整先不动
|
||
final n = normalizeYmd(v);
|
||
if (n != null && v.trim().length >= 8) _applyTyped(n);
|
||
},
|
||
onSubmitted: (v) {
|
||
final n = normalizeYmd(v);
|
||
if (n != null) {
|
||
_syncing = true;
|
||
_ctrl.text = n;
|
||
_syncing = false;
|
||
_applyTyped(n);
|
||
}
|
||
},
|
||
),
|
||
),
|
||
WheelDatePanel(
|
||
key: ValueKey('${widget.title}-${formatYmd(_wheelAnchor)}'),
|
||
initial: _wheelAnchor,
|
||
showFooter: false,
|
||
onCommit: (_) {},
|
||
onChanged: (d) {
|
||
widget.onChanged(d);
|
||
_syncing = true;
|
||
_ctrl.text = formatYmd(d);
|
||
_syncing = false;
|
||
},
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|