diff --git a/client/lib/providers/finance_provider.dart b/client/lib/providers/finance_provider.dart index b1720fd..add373b 100644 --- a/client/lib/providers/finance_provider.dart +++ b/client/lib/providers/finance_provider.dart @@ -22,6 +22,7 @@ class FinanceListNotifier extends AsyncNotifier> { String _month = ''; String _startDate = ''; String _endDate = ''; + int? _partnerId; @override Future> build() { @@ -35,6 +36,7 @@ class FinanceListNotifier extends AsyncNotifier> { month: _month.isEmpty ? null : _month, startDate: _startDate.isEmpty ? null : _startDate, endDate: _endDate.isEmpty ? null : _endDate, + partnerId: _partnerId, page: _page, pageSize: _pageSize, ); @@ -46,6 +48,13 @@ class FinanceListNotifier extends AsyncNotifier> { reload(); } + /// 往来单位筛选(工具栏 ComboSearchField / 移动端 chip→sheet 共用)。 + void setPartner(int? partnerId) { + _partnerId = partnerId; + _page = 1; + reload(); + } + void setMonth(String month) { _month = month; _startDate = ''; diff --git a/client/lib/screens/finance/finance_screen.dart b/client/lib/screens/finance/finance_screen.dart index a088057..557cd3f 100644 --- a/client/lib/screens/finance/finance_screen.dart +++ b/client/lib/screens/finance/finance_screen.dart @@ -15,18 +15,24 @@ import '../../core/theme/context_tokens.dart'; import '../../core/utils/clock.dart'; import '../../core/utils/export_util.dart'; import '../../models/finance.dart'; +import '../../models/partner.dart'; import '../../providers/finance_provider.dart'; +import '../../providers/partner_provider.dart' show allPartnersProvider; import '../../providers/stock_in_provider.dart'; import '../../providers/stock_out_provider.dart'; +import '../../widgets/combo_search_field.dart'; +import '../../widgets/searchable_option_field.dart' show OptionItem; import '../../widgets/ds/ds_atoms.dart'; import '../../widgets/ds/ds_bar_chart.dart'; import '../../widgets/ds/ds_kpi.dart'; import '../../widgets/ds/ds_table.dart'; import '../../widgets/ds/m_kpi_grid.dart'; +import '../../widgets/ds/m_sheet.dart'; import '../../widgets/ds/status_icon_map.dart'; import '../../widgets/finance_entry_dialog.dart'; import '../../widgets/finance_partner_drawer.dart'; import '../../widgets/mobile_list_card.dart'; +import '../../widgets/wheel_date_picker.dart' show showDateRangeDropdown; import '../../widgets/write_guard.dart'; import '../../core/theme/app_fonts.dart'; import '../../widgets/ds/ds_toast.dart'; @@ -44,6 +50,10 @@ class _FinanceScreenState extends ConsumerState { String _rangeLabel = '本月'; // 流水类型筛选(原型 3 chips 扩到 5) String _flowChip = '全部'; + // 往来单位筛选(桌面 toolbar ComboSearchField / 移动端时间行尾 chip→sheet 共用) + int? _partnerId; + // 自定义时间区间(记住上次选择,重开 showDateRangeDropdown 时回显) + DateTimeRange? _customRange; // 窄屏三分段(原型 m-finance .seg:应收 / 应付 / 流水) int _mSeg = 0; @@ -63,11 +73,14 @@ class _FinanceScreenState extends ConsumerState { super.initState(); // 默认本月(与 KPI 口径一致) WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) _applyRange('本月'); + if (mounted) _applyRange('本月', context); }); } - Future _applyRange(String range) async { + /// [anchorContext] 用于「自定义」分支锚定 showDateRangeDropdown(照 stock_in + /// _dateMenu 写法:传入触发该 chip 的 Builder context,桌面下拉/窄屏 sheet 由 + /// 组件内部自适应);本月/本季分支不弹层,传外层 context 即可。 + Future _applyRange(String range, BuildContext anchorContext) async { final now = appNow(); final notifier = ref.read(financeListProvider.notifier); switch (range) { @@ -87,23 +100,27 @@ class _FinanceScreenState extends ConsumerState { _rangeLabel = '本季'; }); case '自定义': - final picked = await showDateRangePicker( - context: context, - firstDate: DateTime(now.year - 3), - lastDate: now, - initialDateRange: + final picked = await showDateRangeDropdown( + anchorContext, + initial: _customRange ?? DateTimeRange(start: DateTime(now.year, now.month, 1), end: now), ); if (picked == null) return; notifier.setRange(_ymd.format(picked.start), _ymd.format(picked.end)); setState(() { _range = range; + _customRange = picked; _rangeLabel = '${_ymd.format(picked.start)} ~ ${_ymd.format(picked.end)}'; }); } } + void _setPartner(int? partnerId) { + setState(() => _partnerId = partnerId); + ref.read(financeListProvider.notifier).setPartner(partnerId); + } + void _reload() { ref.read(financeListProvider.notifier).reload(); ref.invalidate(financePartnerRowsProvider); @@ -267,33 +284,93 @@ class _FinanceScreenState extends ConsumerState { // ── 时间范围 chips(原型 .timerow)── Widget _timeRow(dynamic t, bool mobile) { + // 移动端时间行尾追加「往来单位」chip(D6:showMSheet 可搜单选,× 清除); + // 桌面往来单位筛选走 _flowTable toolbar 的 ComboSearchField(不在此行)。 + final partnerName = mobile + ? _partnerNameById( + ref.watch(allPartnersProvider).valueOrNull?.data ?? const [], + _partnerId) + : null; + + final children = [ + Text('时间范围', style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)), + const SizedBox(width: 12), + for (final r in const ['本月', '本季']) ...[ + DsChip( + label: r, + selected: _range == r, + caret: false, + onTap: () => _applyRange(r, context)), + const SizedBox(width: 10), + ], + Builder( + // anchorCtx = chip 自身位置:自定义范围下拉/sheet 锚定在 chip 下方 + // (照 stock_in_list_screen.dart _dateMenu 写法) + builder: (anchorCtx) => DsChip( + label: '自定义', + selected: _range == '自定义', + onTap: () => _applyRange('自定义', anchorCtx)), + ), + if (mobile) ...[ + const SizedBox(width: 10), + DsChip( + label: '往来单位', + value: partnerName, + onTap: _openPartnerSheet, + onClear: () => _setPartner(null)), + ], + ]; + + if (mobile) { + children.addAll([ + const SizedBox(width: 10), + WriteGuard( + child: DsButton('登记', + small: true, + icon: LucideIcons.plus, + variant: DsBtnVariant.primary, + onPressed: () => showFinanceEntryDialog(context)), + ), + ]); + // 窄屏内容(label+4 chip+登记)常超宽:整行横向可滚(照 _flowTable 移动 + // 工具栏 chips 横向可滚同款写法),而非把登记钉死右侧裁掉往来单位 chip。 + return Padding( + padding: const EdgeInsets.only(bottom: 18), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, child: Row(children: children)), + ); + } + return Padding( padding: const EdgeInsets.only(bottom: 18), - child: Row(children: [ - Text('时间范围', style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)), - const SizedBox(width: 12), - for (final r in const ['本月', '本季', '自定义']) ...[ - DsChip( - label: r, - selected: _range == r, - caret: r == '自定义', - onTap: () => _applyRange(r)), - const SizedBox(width: 10), - ], - if (mobile) ...[ - const Spacer(), - WriteGuard( - child: DsButton('登记', - small: true, - icon: LucideIcons.plus, - variant: DsBtnVariant.primary, - onPressed: () => showFinanceEntryDialog(context)), - ), - ], - ]), + child: Row(children: children), ); } + String? _partnerNameById(List partners, int? id) { + if (id == null) return null; + for (final p in partners) { + if (p.id == id) return p.name; + } + return null; + } + + /// 移动端「往来单位」chip → 底部 sheet 可搜索单选(D6,对齐桌面 ComboSearchField)。 + Future _openPartnerSheet() async { + final partners = ref.read(allPartnersProvider).valueOrNull?.data ?? const []; + final options = [ + for (final p in partners) OptionItem(id: p.id, name: p.name, code: p.code), + ]; + final sel = await showMSheet( + context, + title: '往来单位', + builder: (_) => _PartnerSheetBody(options: options, selectedId: _partnerId), + ); + if (sel == null || !mounted) return; + // -1 = 清除选择(照 SearchableOptionField 的既有约定) + _setPartner(sel == -1 ? null : sel); + } + // ── KPI 4 卡 ── Widget _kpis(dynamic t, bool mobile) { final outSum = ref.watch(stockOutSummaryProvider).valueOrNull; @@ -547,6 +624,20 @@ class _FinanceScreenState extends ConsumerState { style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)), const SizedBox(width: 12), ...chips, + const SizedBox(width: 10), + // 往来单位:可搜索单选下拉(→ financeListProvider.setPartner;照 + // stock_out_list_screen.dart 客户 ComboSearchField 写法)。仅桌面 + // 工具栏放置,移动端对应筛选在 _timeRow 尾部的「往来单位」chip → sheet。 + ComboSearchField( + options: (ref.watch(allPartnersProvider).valueOrNull?.data ?? + const []) + .map((p) => OptionItem(id: p.id, name: p.name, code: p.code)) + .toList(), + selectedId: _partnerId, + hint: '往来单位', + width: 180, + onChanged: _setPartner, + ), const Spacer(), Text('共 $total 条', style: TextStyle( @@ -869,3 +960,108 @@ class _FinanceScreenState extends ConsumerState { ); } } + +/// 移动端「往来单位」sheet 内容:搜索框 + 全部/逐项列表 + 选中勾(照 +/// combo_search_field.dart _ComboPop 的搜索+勾选样式,装进 showMSheet 底部 +/// 容器而非锚定弹层)。选中返回 id;点「全部往来单位」返回 -1(清除,约定 +/// 同 SearchableOptionField)。 +class _PartnerSheetBody extends StatefulWidget { + final List options; + final int? selectedId; + const _PartnerSheetBody({required this.options, required this.selectedId}); + + @override + State<_PartnerSheetBody> createState() => _PartnerSheetBodyState(); +} + +class _PartnerSheetBodyState extends State<_PartnerSheetBody> { + String _kw = ''; + + @override + Widget build(BuildContext context) { + final t = context.tokens; + final hits = _kw.trim().isEmpty + ? widget.options + : widget.options.where((o) => o.matches(_kw)).toList(); + + Widget optRow({ + required String name, + required bool selected, + required VoidCallback onTap, + }) => + InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 4), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: t.borderSubtle)), + ), + child: Row(children: [ + Expanded( + child: Text(name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: AppDims.fsBody, + fontWeight: + selected ? FontWeight.w600 : FontWeight.w400, + color: selected ? t.primary : t.text)), + ), + if (selected) ...[ + const SizedBox(width: 8), + Icon(LucideIcons.check, size: 18, color: t.primary), + ], + ]), + ), + ); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 9), + decoration: BoxDecoration( + color: t.bg, + borderRadius: BorderRadius.circular(AppDims.rMd), + ), + child: Row(children: [ + Icon(LucideIcons.search, size: 15, color: t.faint), + const SizedBox(width: 8), + Expanded( + child: TextField( + style: TextStyle(fontSize: AppDims.fsBody, color: t.text), + decoration: InputDecoration( + isCollapsed: true, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + hintText: '搜索往来单位…', + hintStyle: + TextStyle(fontSize: AppDims.fsBody, color: t.faint), + ), + onChanged: (v) => setState(() => _kw = v), + ), + ), + ]), + ), + const SizedBox(height: 6), + optRow( + name: '全部往来单位', + selected: widget.selectedId == null, + onTap: () => Navigator.of(context).pop(-1), + ), + if (hits.isEmpty) + Padding( + padding: const EdgeInsets.all(20), + child: Text('无匹配结果', + style: TextStyle(color: t.muted, fontSize: AppDims.fsSm)), + ) + else + for (final o in hits) + optRow( + name: o.name, + selected: o.id == widget.selectedId, + onTap: () => Navigator.of(context).pop(o.id), + ), + ], + ); + } +} diff --git a/client/test/golden/goldens/finance_mobile_a.png b/client/test/golden/goldens/finance_mobile_a.png index f21f762..88918c4 100644 Binary files a/client/test/golden/goldens/finance_mobile_a.png and b/client/test/golden/goldens/finance_mobile_a.png differ diff --git a/client/test/golden/goldens/finance_mobile_b.png b/client/test/golden/goldens/finance_mobile_b.png index 10047d2..98e4fac 100644 Binary files a/client/test/golden/goldens/finance_mobile_b.png and b/client/test/golden/goldens/finance_mobile_b.png differ diff --git a/client/test/golden/goldens/finance_mobile_c.png b/client/test/golden/goldens/finance_mobile_c.png index ee741d0..97e2ae6 100644 Binary files a/client/test/golden/goldens/finance_mobile_c.png and b/client/test/golden/goldens/finance_mobile_c.png differ diff --git a/client/test/golden/goldens/m_finance_a.png b/client/test/golden/goldens/m_finance_a.png index f100c3a..bb3e5a8 100644 Binary files a/client/test/golden/goldens/m_finance_a.png and b/client/test/golden/goldens/m_finance_a.png differ diff --git a/client/test/golden/goldens/m_finance_b.png b/client/test/golden/goldens/m_finance_b.png index e792e1c..0a81169 100644 Binary files a/client/test/golden/goldens/m_finance_b.png and b/client/test/golden/goldens/m_finance_b.png differ diff --git a/client/test/golden/goldens/m_finance_c.png b/client/test/golden/goldens/m_finance_c.png index 11575e9..6e5ea7a 100644 Binary files a/client/test/golden/goldens/m_finance_c.png and b/client/test/golden/goldens/m_finance_c.png differ diff --git a/design/prototype/screens/finance.html b/design/prototype/screens/finance.html index 516b653..f734d24 100644 --- a/design/prototype/screens/finance.html +++ b/design/prototype/screens/finance.html @@ -83,7 +83,7 @@ 时间范围
本月
本季
-
自定义
+
自定义
@@ -123,6 +123,7 @@
全部
收款
付款
+
往来单位
@@ -141,7 +142,16 @@ + diff --git a/design/prototype/screens/m-finance.html b/design/prototype/screens/m-finance.html index b5cb254..b36d612 100644 --- a/design/prototype/screens/m-finance.html +++ b/design/prototype/screens/m-finance.html @@ -7,12 +7,30 @@ - +
+ +
+ 时间范围 +
本月
+
本季
+
自定义
+
往来单位
+
登记
+
本月收入
¥86.4万
▲ 8.2% 较上月
本月支出
¥52.1万
▲ 3.1% 较上月
@@ -29,6 +47,7 @@ +