fix(client): 财务汇总下拉候选改全量往来单位并修占位叠词与表头垂直居中(验收反馈)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-14 13:10:11 +08:00
parent 4bd083f2eb
commit 94a5f437a5
7 changed files with 174 additions and 139 deletions
+119 -116
View File
@@ -63,7 +63,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
String _summaryKw = '';
// 桌面汇总表表头「往来单位」下拉筛选(验收反馈改版:由卡标题行文字搜索
// 改为表头内嵌 ComboSearchField,同 _flowTable 往来单位筛选控件;单选精确
// 匹配一行,× 清除恢复全量;候选=当前汇总行往来单位,口径=显示口径)。
// 匹配一行,× 清除恢复全量;候选=全量往来单位(allPartnersProvider,与
// _flowTable toolbar 同源),选中一个汇总里没有行的单位时显示空态)。
int? _summaryPartnerId;
static const _flowChips = ['全部', '应收', '应付', '收款', '付款'];
@@ -111,8 +112,7 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
case '自定义':
final picked = await showDateRangeDropdown(
anchorContext,
initial:
_customRange ??
initial: _customRange ??
DateTimeRange(start: DateTime(now.year, now.month, 1), end: now),
);
if (picked == null) return;
@@ -575,15 +575,15 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
final t = context.tokens;
final trend = ref.watch(financeTrendProvider);
Widget placeholder(Widget child) => Container(
height: 120,
alignment: Alignment.center,
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rLg),
),
child: child,
);
height: 120,
alignment: Alignment.center,
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rLg),
),
child: child,
);
return trend.when(
loading: () => placeholder(
const SizedBox(
@@ -640,19 +640,17 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
),
data: (allRows) {
final rows = _filterSummaryByPartner(allRows);
// 下拉候选=当前汇总行往来单位(口径=显示口径,不用全量 partners,避免
// 选到无余额单位出空表);id 合成规则同 _filterSummaryByPartner。
final partnerOptions = <OptionItem>[
for (var i = 0; i < allRows.length; i++)
OptionItem(
id: allRows[i].partnerId ?? -(i + 1),
name: allRows[i].name,
),
];
// 下拉候选=全量往来单位(验收反馈:与 _flowTable toolbar 往来单位筛选
// 同源 allPartnersProvider,而非只列汇总行里出现的单位);选中一个汇总
// 无余额的单位时,_filterSummaryByPartner 匹配不到行会返回空列表,
// 走既有「没有匹配的往来单位」空态。
final partnerOptions =
(ref.watch(allPartnersProvider).valueOrNull?.data ?? const [])
.map((p) => OptionItem(id: p.id, name: p.name, code: p.code))
.toList();
return DsTable(
shrinkWrap: true,
emptyText:
_summaryPartnerId == null ? '暂无未结清的应收 / 应付' : '没有匹配的往来单位',
emptyText: _summaryPartnerId == null ? '暂无未结清的应收 / 应付' : '没有匹配的往来单位',
mobileCards: rows
.map(
(r) => MobileListCard(
@@ -682,26 +680,35 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
'往来单位',
// 表头「往来单位」文字右边内嵌下拉(验收反馈:由卡标题行搜索框
// 改到此处,与 _flowTable 往来单位筛选同款 ComboSearchField)。
header: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'往来单位',
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600,
color: t.muted,
// 外包 SizedBox 固定高=ComboSearchField 自身高度:Row 的
// crossAxisAlignment.center 依赖两个子项各自的内在高度居中,
// 标签文字行高在不同字体下可能与 34 有偏差,导致下拉在表头行
// 里视觉偏上(验收反馈);显式钉住 Row 高度消除该偏差,不改
// 表头行 pad 11/14 的既有行高口径。
header: SizedBox(
height: 34,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'往来单位',
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600,
color: t.muted,
),
),
),
const SizedBox(width: 10),
ComboSearchField(
options: partnerOptions,
selectedId: _summaryPartnerId,
hint: '搜索往来单位',
width: 180,
onChanged: _setSummaryPartner,
),
],
const SizedBox(width: 10),
ComboSearchField(
options: partnerOptions,
selectedId: _summaryPartnerId,
hint: '往来单位',
width: 180,
onChanged: _setSummaryPartner,
),
],
),
),
),
const DsColumn('recv', '应收', numeric: true),
@@ -740,9 +747,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback,
fontWeight: FontWeight.w600,
color: r.net > 0
? t.success
: (r.net < 0 ? t.danger : t.text),
color:
r.net > 0 ? t.success : (r.net < 0 ? t.danger : t.text),
),
),
InkWell(
@@ -806,14 +812,12 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
// 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(),
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,
@@ -971,8 +975,7 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
MobileCardField('状态', r.status == 'open' ? '未结清' : '已结清'),
if (r.remark?.isNotEmpty == true) MobileCardField('备注', r.remark),
],
actions:
(r.status == 'open' &&
actions: (r.status == 'open' &&
(r.type == 'receivable' || r.type == 'payable'))
? [
WriteGuard(
@@ -990,23 +993,22 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
/// delta 箭头/色调映射(与 DsKpi 同规则:▲/▼ 文本字符)。
String _mDeltaText(String text, DsKpiDelta tone) => switch (tone) {
DsKpiDelta.up => '$text',
DsKpiDelta.down => '$text',
DsKpiDelta.neutral => text,
};
DsKpiDelta.up => '$text',
DsKpiDelta.down => '$text',
DsKpiDelta.neutral => text,
};
MKpiDeltaTone _mTone(DsKpiDelta tone) => switch (tone) {
DsKpiDelta.up => MKpiDeltaTone.up,
DsKpiDelta.down => MKpiDeltaTone.down,
DsKpiDelta.neutral => MKpiDeltaTone.normal,
};
DsKpiDelta.up => MKpiDeltaTone.up,
DsKpiDelta.down => MKpiDeltaTone.down,
DsKpiDelta.neutral => MKpiDeltaTone.normal,
};
/// 窄屏 KPI 2×2(原型 .m-kpi:本月收入 / 本月支出 / 应收合计 / 应付合计)。
Widget _mKpis(dynamic t) {
final outSum = ref.watch(stockOutSummaryProvider).valueOrNull;
final inSum = ref.watch(stockInSummaryProvider).valueOrNull;
final rows =
ref.watch(financePartnerRowsProvider).valueOrNull ??
final rows = ref.watch(financePartnerRowsProvider).valueOrNull ??
const <PartnerFinanceRow>[];
var recv = 0.0, pay = 0.0, openCount = 0;
for (final r in rows) {
@@ -1054,32 +1056,32 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
/// 三分段列表体:m-section 计数标题 + 卡片流(应收/应付=往来汇总,流水=收支记录)。
List<Widget> _mSegBody(dynamic t, List<FinanceRecord> flows) {
Widget section(String title, int count) => Padding(
padding: const EdgeInsets.fromLTRB(2, 16, 2, 8),
child: Text(
'$title · 共 $count',
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w700,
letterSpacing: .4,
color: t.muted,
),
),
);
padding: const EdgeInsets.fromLTRB(2, 16, 2, 8),
child: Text(
'$title · 共 $count',
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w700,
letterSpacing: .4,
color: t.muted,
),
),
);
Widget empty([String msg = '暂无数据']) => Padding(
padding: const EdgeInsets.all(24),
child: Center(
child: Text(
msg,
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm),
),
),
);
padding: const EdgeInsets.all(24),
child: Center(
child: Text(
msg,
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm),
),
),
);
List<Widget> stacked(List<Widget> cards) => [
for (var i = 0; i < cards.length; i++) ...[
if (i > 0) const SizedBox(height: 10),
cards[i],
],
];
for (var i = 0; i < cards.length; i++) ...[
if (i > 0) const SizedBox(height: 10),
cards[i],
],
];
if (_mSeg == 2) {
return [
@@ -1216,34 +1218,35 @@ class _PartnerSheetBodyState extends State<_PartnerSheetBody> {
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,
),
),
}) =>
InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 4),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: t.borderSubtle)),
),
if (selected) ...[
const SizedBox(width: 8),
Icon(LucideIcons.check, size: 18, color: t.primary),
],
],
),
),
);
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,