feat(client): 日期范围改锚定下拉——起止并排双滚轮 + 可键入,不再冻结窗口

- 新增 showDateRangeDropdown:锚定触发控件下方(透明遮罩点外关闭,形态同
  DsMenu 下拉),起始/结束两列并排,各带可输入框(键入/回车/失焦归一
  yyyy-MM-dd)与滚轮,输入↔滚轮双向联动;共用底栏显示当前范围 + 确定,
  起止倒置自动交换;窄屏回落既有两步滚轮弹窗
- WheelDatePanel 加 showFooter/onChanged(滚动即回调),供范围面板复用
- 出入库列表 6 处调用切换:工具栏时间 chip「自定义…」、桌面详搜弹窗、
  窄屏详搜 sheet 的日期区间字段均锚定在各自控件下方

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-04 17:29:21 +08:00
parent 0425939d7e
commit 06dc68245a
3 changed files with 533 additions and 189 deletions
@@ -198,8 +198,8 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
n.setDetail(const {});
}
Future<void> _pickDateRange() async {
final range = await showWheelDateRange(context, initial: _dateRange);
Future<void> _pickDateRange(BuildContext anchor) async {
final range = await showDateRangeDropdown(anchor, initial: _dateRange);
if (range != null) {
setState(() {
_dateRange = range;
@@ -210,9 +210,9 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
}
/// 入库时间预设(对齐原型:全部时间/近7天/近30天/本月/自定义)。
void _setDatePreset(String preset) {
void _setDatePreset(BuildContext anchor, String preset) {
if (preset == 'custom') {
_pickDateRange();
_pickDateRange(anchor);
return;
}
final now = DateTime.now();
@@ -245,26 +245,29 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
ref.read(stockInListProvider.notifier).setDateRange(_startDate, _endDate);
}
Widget _dateMenu(BuildContext ctx) => PopupMenuButton<String>(
onSelected: _setDatePreset,
offset: const Offset(0, 40),
color: ctx.tokens.surface,
elevation: 8,
shape: RoundedRectangleBorder(
side: BorderSide(color: ctx.tokens.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
Widget _dateMenu(BuildContext ctx) => Builder(
// anchorCtx = chip 自身位置:自定义范围下拉锚定在 chip 下方
builder: (anchorCtx) => PopupMenuButton<String>(
onSelected: (v) => _setDatePreset(anchorCtx, v),
offset: const Offset(0, 40),
color: ctx.tokens.surface,
elevation: 8,
shape: RoundedRectangleBorder(
side: BorderSide(color: ctx.tokens.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
itemBuilder: (_) => const [
PopupMenuItem(value: 'all', height: 38, child: Text('全部时间')),
PopupMenuItem(value: '7', height: 38, child: Text('近 7 天')),
PopupMenuItem(value: '30', height: 38, child: Text('近 30 天')),
PopupMenuItem(value: 'month', height: 38, child: Text('本月')),
PopupMenuItem(value: 'custom', height: 38, child: Text('自定义…')),
],
child: DsChip(
label: '入库时间',
value: _datePresetLabel.isEmpty ? null : _datePresetLabel,
onClear: () => _setDatePreset(anchorCtx, 'all')),
),
itemBuilder: (_) => const [
PopupMenuItem(value: 'all', height: 38, child: Text('全部时间')),
PopupMenuItem(value: '7', height: 38, child: Text('近 7 天')),
PopupMenuItem(value: '30', height: 38, child: Text('近 30 天')),
PopupMenuItem(value: 'month', height: 38, child: Text('本月')),
PopupMenuItem(value: 'custom', height: 38, child: Text('自定义…')),
],
child: DsChip(
label: '入库时间',
value: _datePresetLabel.isEmpty ? null : _datePresetLabel,
onClear: () => _setDatePreset('all')),
);
Future<void> _openAdvSearch() async {
@@ -1649,8 +1652,8 @@ class _AdvSearchDialogState extends ConsumerState<_AdvSearchDialog> {
Navigator.of(context).pop(_AdvResult(d, _status, _range));
}
Future<void> _pickDate() async {
final range = await showWheelDateRange(context, initial: _range);
Future<void> _pickDate(BuildContext anchor) async {
final range = await showDateRangeDropdown(anchor, initial: _range);
if (range != null) setState(() => _range = range);
}
@@ -1930,33 +1933,35 @@ class _AdvSearchDialogState extends ConsumerState<_AdvSearchDialog> {
final label = has
? '${_range!.start.toString().substring(0, 10)} ~ ${_range!.end.toString().substring(0, 10)}'
: '全部时间';
return InkWell(
onTap: _pickDate,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: 38,
padding: const EdgeInsets.symmetric(horizontal: 11),
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(children: [
Expanded(
child: Text(label,
style: TextStyle(
fontSize: AppDims.fsBody, color: has ? t.text : t.faint)),
),
if (has)
GestureDetector(
onTap: () => setState(() => _range = null),
child: Icon(LucideIcons.x, size: 14, color: t.muted),
)
else
Icon(LucideIcons.calendar, size: 16, color: t.faint),
]),
),
);
return Builder(
builder: (anchorCtx) => InkWell(
onTap: () => _pickDate(anchorCtx),
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: 38,
padding: const EdgeInsets.symmetric(horizontal: 11),
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(children: [
Expanded(
child: Text(label,
style: TextStyle(
fontSize: AppDims.fsBody,
color: has ? t.text : t.faint)),
),
if (has)
GestureDetector(
onTap: () => setState(() => _range = null),
child: Icon(LucideIcons.x, size: 14, color: t.muted),
)
else
Icon(LucideIcons.calendar, size: 16, color: t.faint),
]),
),
));
}
}
@@ -2055,8 +2060,8 @@ class _AdvSearchSheetState extends ConsumerState<_AdvSearchSheet> {
Navigator.of(context).pop(_AdvResult(d, _status, _range));
}
Future<void> _pickDate() async {
final range = await showWheelDateRange(context, initial: _range);
Future<void> _pickDate(BuildContext anchor) async {
final range = await showDateRangeDropdown(anchor, initial: _range);
if (range != null) setState(() => _range = range);
}
@@ -2171,37 +2176,39 @@ class _AdvSearchSheetState extends ConsumerState<_AdvSearchSheet> {
final statusSelIdx = _statusCodes.indexOf(_status);
final dateHas = _range != null;
final dateField = InkWell(
onTap: _pickDate,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: 42,
padding: const EdgeInsets.symmetric(horizontal: 11),
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(children: [
Expanded(
child: Text(
dateHas
? '${_range!.start.toString().substring(0, 10)} ~ ${_range!.end.toString().substring(0, 10)}'
: '全部时间',
style: TextStyle(
fontSize: AppDims.fsBody, color: dateHas ? t.text : t.faint),
),
),
if (dateHas)
GestureDetector(
onTap: () => setState(() => _range = null),
child: Icon(LucideIcons.x, size: 14, color: t.muted),
)
else
Icon(LucideIcons.calendar, size: 16, color: t.faint),
]),
),
);
final dateField = Builder(
builder: (anchorCtx) => InkWell(
onTap: () => _pickDate(anchorCtx),
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: 42,
padding: const EdgeInsets.symmetric(horizontal: 11),
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(children: [
Expanded(
child: Text(
dateHas
? '${_range!.start.toString().substring(0, 10)} ~ ${_range!.end.toString().substring(0, 10)}'
: '全部时间',
style: TextStyle(
fontSize: AppDims.fsBody,
color: dateHas ? t.text : t.faint),
),
),
if (dateHas)
GestureDetector(
onTap: () => setState(() => _range = null),
child: Icon(LucideIcons.x, size: 14, color: t.muted),
)
else
Icon(LucideIcons.calendar, size: 16, color: t.faint),
]),
),
));
return Column(
mainAxisSize: MainAxisSize.min,
@@ -1713,8 +1713,8 @@ class _AdvancedSearchDialogState extends ConsumerState<_AdvancedSearchDialog> {
));
}
Future<void> _pickDate() async {
final range = await showWheelDateRange(context, initial: _dateRange);
Future<void> _pickDate(BuildContext anchor) async {
final range = await showDateRangeDropdown(anchor, initial: _dateRange);
if (range != null) setState(() => _dateRange = range);
}
@@ -1838,38 +1838,39 @@ class _AdvancedSearchDialogState extends ConsumerState<_AdvancedSearchDialog> {
}
final dateHas = _dateRange != null;
Widget dateField() => InkWell(
onTap: _pickDate,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: 38,
padding: const EdgeInsets.symmetric(horizontal: 11),
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
Widget dateField() => Builder(
builder: (anchorCtx) => InkWell(
onTap: () => _pickDate(anchorCtx),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(children: [
Expanded(
child: Text(
dateHas
? '${DateFormat('yyyy-MM-dd').format(_dateRange!.start)} ~ ${DateFormat('yyyy-MM-dd').format(_dateRange!.end)}'
: '全部时间',
style: TextStyle(
fontSize: AppDims.fsBody,
color: dateHas ? t.text : t.faint),
child: Container(
height: 38,
padding: const EdgeInsets.symmetric(horizontal: 11),
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(children: [
Expanded(
child: Text(
dateHas
? '${DateFormat('yyyy-MM-dd').format(_dateRange!.start)} ~ ${DateFormat('yyyy-MM-dd').format(_dateRange!.end)}'
: '全部时间',
style: TextStyle(
fontSize: AppDims.fsBody,
color: dateHas ? t.text : t.faint),
),
),
if (dateHas)
GestureDetector(
onTap: () => setState(() => _dateRange = null),
child: Icon(LucideIcons.x, size: 14, color: t.muted),
)
else
Icon(LucideIcons.calendar, size: 16, color: t.faint),
]),
),
if (dateHas)
GestureDetector(
onTap: () => setState(() => _dateRange = null),
child: Icon(LucideIcons.x, size: 14, color: t.muted),
)
else
Icon(LucideIcons.calendar, size: 16, color: t.faint),
]),
),
);
));
return AlertDialog(
backgroundColor: t.surface,
@@ -2135,8 +2136,8 @@ class _AdvSearchSheetState extends ConsumerState<_AdvSearchSheet> {
));
}
Future<void> _pickDate() async {
final range = await showWheelDateRange(context, initial: _dateRange);
Future<void> _pickDate(BuildContext anchor) async {
final range = await showDateRangeDropdown(anchor, initial: _dateRange);
if (range != null) setState(() => _dateRange = range);
}
@@ -2225,37 +2226,39 @@ class _AdvSearchSheetState extends ConsumerState<_AdvSearchSheet> {
final statusSelIdx = _statusCodes.indexOf(_status);
final dateHas = _dateRange != null;
final dateField = InkWell(
onTap: _pickDate,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: 42,
padding: const EdgeInsets.symmetric(horizontal: 11),
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(children: [
Expanded(
child: Text(
dateHas
? '${DateFormat('yyyy-MM-dd').format(_dateRange!.start)} ~ ${DateFormat('yyyy-MM-dd').format(_dateRange!.end)}'
: '全部时间',
style: TextStyle(
fontSize: AppDims.fsBody, color: dateHas ? t.text : t.faint),
),
),
if (dateHas)
GestureDetector(
onTap: () => setState(() => _dateRange = null),
child: Icon(LucideIcons.x, size: 14, color: t.muted),
)
else
Icon(LucideIcons.calendar, size: 16, color: t.faint),
]),
),
);
final dateField = Builder(
builder: (anchorCtx) => InkWell(
onTap: () => _pickDate(anchorCtx),
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: 42,
padding: const EdgeInsets.symmetric(horizontal: 11),
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(children: [
Expanded(
child: Text(
dateHas
? '${DateFormat('yyyy-MM-dd').format(_dateRange!.start)} ~ ${DateFormat('yyyy-MM-dd').format(_dateRange!.end)}'
: '全部时间',
style: TextStyle(
fontSize: AppDims.fsBody,
color: dateHas ? t.text : t.faint),
),
),
if (dateHas)
GestureDetector(
onTap: () => setState(() => _dateRange = null),
child: Icon(LucideIcons.x, size: 14, color: t.muted),
)
else
Icon(LucideIcons.calendar, size: 16, color: t.faint),
]),
),
));
return Column(
mainAxisSize: MainAxisSize.min,