feat(client): 财务筛选增加往来单位并统一自定义时间组件(两端,原型同步)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-14 10:09:22 +08:00
parent 782cf057a3
commit a2aca2e7fc
10 changed files with 314 additions and 35 deletions
@@ -22,6 +22,7 @@ class FinanceListNotifier extends AsyncNotifier<PageResult<FinanceRecord>> {
String _month = '';
String _startDate = '';
String _endDate = '';
int? _partnerId;
@override
Future<PageResult<FinanceRecord>> build() {
@@ -35,6 +36,7 @@ class FinanceListNotifier extends AsyncNotifier<PageResult<FinanceRecord>> {
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<PageResult<FinanceRecord>> {
reload();
}
/// 往来单位筛选(工具栏 ComboSearchField / 移动端 chip→sheet 共用)。
void setPartner(int? partnerId) {
_partnerId = partnerId;
_page = 1;
reload();
}
void setMonth(String month) {
_month = month;
_startDate = '';
+225 -29
View File
@@ -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<FinanceScreen> {
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<FinanceScreen> {
super.initState();
// 默认本月(与 KPI 口径一致)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _applyRange('本月');
if (mounted) _applyRange('本月', context);
});
}
Future<void> _applyRange(String range) async {
/// [anchorContext] 用于「自定义」分支锚定 showDateRangeDropdown(照 stock_in
/// _dateMenu 写法:传入触发该 chip 的 Builder context,桌面下拉/窄屏 sheet 由
/// 组件内部自适应);本月/本季分支不弹层,传外层 context 即可。
Future<void> _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<FinanceScreen> {
_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<FinanceScreen> {
// ── 时间范围 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 = <Widget>[
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<Partner> 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<void> _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<int?>(
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<FinanceScreen> {
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<FinanceScreen> {
);
}
}
/// 移动端「往来单位」sheet 内容:搜索框 + 全部/逐项列表 + 选中勾(照
/// combo_search_field.dart _ComboPop 的搜索+勾选样式,装进 showMSheet 底部
/// 容器而非锚定弹层)。选中返回 id;点「全部往来单位」返回 -1(清除,约定
/// 同 SearchableOptionField)。
class _PartnerSheetBody extends StatefulWidget {
final List<OptionItem> 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),
),
],
);
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 102 KiB