fix(client): 财务汇总下拉候选改全量往来单位并修占位叠词与表头垂直居中(验收反馈)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -77,6 +77,9 @@ class _FakeFinanceRepository extends FinanceRepository {
|
||||
Future<List<TrendPoint>> trend({int months = 6}) async => const [];
|
||||
}
|
||||
|
||||
/// 第 4 条「未挂账供应商」故意不出现在 partnerSummary() 里,用于验证下拉
|
||||
/// 候选=全量往来单位(而非只列汇总行里出现的单位):选中它后汇总应展示
|
||||
/// 既有「没有匹配的往来单位」空态。
|
||||
class _FakeAllPartners extends PartnerListNotifier {
|
||||
@override
|
||||
Future<PageResult<Partner>> build() async => const PageResult(
|
||||
@@ -84,8 +87,9 @@ class _FakeAllPartners extends PartnerListNotifier {
|
||||
Partner(id: 1, name: '华东酒类批发', type: 'customer'),
|
||||
Partner(id: 2, name: '金樽商贸', type: 'customer'),
|
||||
Partner(id: 3, name: '茅台华南经销', type: 'supplier'),
|
||||
Partner(id: 4, name: '未挂账供应商', type: 'supplier'),
|
||||
],
|
||||
total: 3,
|
||||
total: 4,
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
);
|
||||
@@ -94,10 +98,10 @@ class _FakeAllPartners extends PartnerListNotifier {
|
||||
List<Override> _overrides() => [
|
||||
financeRepositoryProvider.overrideWith((ref) => _FakeFinanceRepository()),
|
||||
allPartnersProvider.overrideWith(() => _FakeAllPartners()),
|
||||
stockOutSummaryProvider.overrideWith(
|
||||
(ref) async => const StockSummary(monthAmount: 0, lastMonthAmount: 0)),
|
||||
stockInSummaryProvider.overrideWith(
|
||||
(ref) async => const StockSummary(monthAmount: 0, lastMonthAmount: 0)),
|
||||
stockOutSummaryProvider.overrideWith((ref) async =>
|
||||
const StockSummary(monthAmount: 0, lastMonthAmount: 0)),
|
||||
stockInSummaryProvider.overrideWith((ref) async =>
|
||||
const StockSummary(monthAmount: 0, lastMonthAmount: 0)),
|
||||
isReadonlyProvider.overrideWithValue(false),
|
||||
];
|
||||
|
||||
@@ -134,13 +138,13 @@ void main() {
|
||||
expect(find.text('金樽商贸'), findsOneWidget);
|
||||
expect(find.text('茅台华南经销'), findsOneWidget);
|
||||
|
||||
// 定位表头内嵌的 ComboSearchField(按构造参数 hint 匹配,避免与
|
||||
// _flowTable toolbar 的往来单位 ComboSearchField(hint:'往来单位')
|
||||
// 混淆;选中后触发器文案会变成往来单位名,故不能用显示文字定位)。
|
||||
final combo = find.byWidgetPredicate(
|
||||
(w) => w is ComboSearchField && w.hint == '搜索往来单位',
|
||||
);
|
||||
expect(combo, findsOneWidget);
|
||||
// 定位表头内嵌的 ComboSearchField:验收反馈后候选/占位文案与
|
||||
// _flowTable toolbar 的往来单位 ComboSearchField 完全同款(同为
|
||||
// hint:'往来单位'),不能再靠 hint/显示文字区分;改用构建顺序——
|
||||
// 汇总表(_summaryTable)先于流水表(_flowTable)出现在页面里,
|
||||
// 故 ComboSearchField 的第一个即汇总表头这个。
|
||||
final combo = find.byType(ComboSearchField).first;
|
||||
expect(find.byType(ComboSearchField), findsNWidgets(2));
|
||||
await tester.ensureVisible(combo);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -168,6 +172,30 @@ void main() {
|
||||
expect(find.text('金樽商贸'), findsOneWidget);
|
||||
expect(find.text('茅台华南经销'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('候选=全量往来单位;选中汇总里没有行的单位显示空态', (tester) async {
|
||||
await _pumpFinance(tester, size: const Size(1280, 900));
|
||||
|
||||
final combo = find.byType(ComboSearchField).first;
|
||||
await tester.ensureVisible(combo);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(combo);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 候选来自全量往来单位(4 条,含汇总里没有余额行的「未挂账供应商」),
|
||||
// 而不再是「共 3 条结果」(旧口径=当前汇总行往来单位)。
|
||||
expect(find.text('共 4 条结果'), findsOneWidget);
|
||||
expect(find.widgetWithText(InkWell, '未挂账供应商'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.widgetWithText(InkWell, '未挂账供应商'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 该单位在汇总里没有余额行 → 走既有「没有匹配的往来单位」空态。
|
||||
expect(find.text('没有匹配的往来单位'), findsOneWidget);
|
||||
expect(find.text('华东酒类批发'), findsNothing);
|
||||
expect(find.text('金樽商贸'), findsNothing);
|
||||
expect(find.text('茅台华南经销'), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
group('移动应收/应付分段往来单位搜索', () {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 283 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 280 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 283 KiB |
Reference in New Issue
Block a user