feat(client): 财务应收应付汇总增加往来单位搜索(两端,原型同步)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-14 12:02:44 +08:00
parent 6069b390df
commit edb85c88da
14 changed files with 762 additions and 360 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,194 @@
// test/finance_summary_search_test.dart — 财务「应收/应付 汇总」往来单位搜索回归。
//
// financePartnerRowsProviderGET /finance/summary)不分页、一次性全量加载,
// 搜索口径=显示口径:仅需验证本地 contains 过滤在已加载行上生效——桌面
// _summaryTable 工具头 DsSearchBox / 移动 应收·应付 分段头 MSearchRow 均覆盖:
// 1. 输入关键字 → 汇总行按往来单位名过滤,未命中不显示;
// 2. 关键字不匹配任何行 → 展示「没有匹配的往来单位」空态;
// 3. 清空关键字 → 恢复全量展示。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:jiu_client/core/api/api_client.dart';
import 'package:jiu_client/core/auth/auth_state.dart';
import 'package:jiu_client/core/models/page_result.dart';
import 'package:jiu_client/core/theme/themes.dart';
import 'package:jiu_client/models/finance.dart';
import 'package:jiu_client/models/partner.dart';
import 'package:jiu_client/models/stock_summary.dart';
import 'package:jiu_client/providers/finance_provider.dart';
import 'package:jiu_client/providers/partner_provider.dart';
import 'package:jiu_client/providers/stock_in_provider.dart';
import 'package:jiu_client/providers/stock_out_provider.dart';
import 'package:jiu_client/repositories/finance_repository.dart';
import 'package:jiu_client/screens/finance/finance_screen.dart';
import 'package:jiu_client/widgets/ds/ds_atoms.dart' show DsSearchBox;
import 'package:jiu_client/widgets/ds/m_search_row.dart' show MSearchRow;
/// 不打真实网络:仅用于满足 FinanceRepository 构造签名,方法全部被覆写。
class _DummyApiClient extends ApiClient {
_DummyApiClient() : super(token: 'test-token');
}
/// 固定 3 条往来单位汇总(2 应收 + 1 应付),流水列表恒空(本测试不关心流水)。
class _FakeFinanceRepository extends FinanceRepository {
_FakeFinanceRepository() : super(_DummyApiClient());
@override
Future<PageResult<FinanceRecord>> listRecords({
String? type,
String? month,
String? startDate,
String? endDate,
int? partnerId,
int page = 1,
int pageSize = 50,
}) async =>
const PageResult(data: [], total: 0, page: 1, pageSize: 50);
@override
Future<List<PartnerFinanceSummary>> partnerSummary() async => const [
PartnerFinanceSummary(
partnerId: 1,
partnerName: '华东酒类批发',
type: 'receivable',
recordCount: 2,
totalAmount: 128400),
PartnerFinanceSummary(
partnerId: 2,
partnerName: '金樽商贸',
type: 'receivable',
recordCount: 1,
totalAmount: 86200),
PartnerFinanceSummary(
partnerId: 3,
partnerName: '茅台华南经销',
type: 'payable',
recordCount: 1,
totalAmount: 142800),
];
@override
Future<List<TrendPoint>> trend({int months = 6}) async => const [];
}
class _FakeAllPartners extends PartnerListNotifier {
@override
Future<PageResult<Partner>> build() async => const PageResult(
data: [
Partner(id: 1, name: '华东酒类批发', type: 'customer'),
Partner(id: 2, name: '金樽商贸', type: 'customer'),
Partner(id: 3, name: '茅台华南经销', type: 'supplier'),
],
total: 3,
page: 1,
pageSize: 1000,
);
}
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)),
isReadonlyProvider.overrideWithValue(false),
];
Future<void> _pumpFinance(
WidgetTester tester, {
required Size size,
}) async {
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(ProviderScope(
overrides: _overrides(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: buildTheme('a'),
home: const Scaffold(body: FinanceScreen()),
),
));
await tester.pumpAndSettle();
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
group('桌面汇总卡往来单位搜索', () {
testWidgets('输入关键字过滤汇总行,无匹配报空态,清空恢复全量', (tester) async {
await _pumpFinance(tester, size: const Size(1280, 900));
// 初始:3 条汇总行全部展示。
expect(find.text('华东酒类批发'), findsOneWidget);
expect(find.text('金樽商贸'), findsOneWidget);
expect(find.text('茅台华南经销'), findsOneWidget);
final searchField = find.descendant(
of: find.byType(DsSearchBox),
matching: find.byType(TextField),
);
expect(searchField, findsOneWidget);
// 输入「金樽」:只剩「金樽商贸」。
await tester.enterText(searchField, '金樽');
await tester.pumpAndSettle();
expect(find.text('金樽商贸'), findsOneWidget);
expect(find.text('华东酒类批发'), findsNothing);
expect(find.text('茅台华南经销'), findsNothing);
// 输入不存在的关键字:空态提示。
await tester.enterText(searchField, '不存在的单位名');
await tester.pumpAndSettle();
expect(find.text('没有匹配的往来单位'), findsOneWidget);
expect(find.text('金樽商贸'), findsNothing);
// 清空:恢复全量 3 行。
await tester.enterText(searchField, '');
await tester.pumpAndSettle();
expect(find.text('华东酒类批发'), findsOneWidget);
expect(find.text('金樽商贸'), findsOneWidget);
expect(find.text('茅台华南经销'), findsOneWidget);
});
});
group('移动应收/应付分段往来单位搜索', () {
testWidgets('应收分段输入关键字过滤卡片,清空恢复全量', (tester) async {
await _pumpFinance(tester, size: const Size(390, 1600));
// 默认「应收」分段:仅展示 recv>0 的两条(华东/金樽),不含应付的茅台。
expect(find.text('华东酒类批发'), findsOneWidget);
expect(find.text('金樽商贸'), findsOneWidget);
expect(find.text('茅台华南经销'), findsNothing);
final searchField = find.descendant(
of: find.byType(MSearchRow),
matching: find.byType(TextField),
);
expect(searchField, findsOneWidget);
await tester.enterText(searchField, '金樽');
await tester.pumpAndSettle();
expect(find.text('金樽商贸'), findsOneWidget);
expect(find.text('华东酒类批发'), findsNothing);
// 无匹配 → 分段空态。
await tester.enterText(searchField, '不存在的单位名');
await tester.pumpAndSettle();
expect(find.text('没有匹配的往来单位'), findsOneWidget);
// 清空恢复。
await tester.enterText(searchField, '');
await tester.pumpAndSettle();
expect(find.text('华东酒类批发'), findsOneWidget);
expect(find.text('金樽商贸'), findsOneWidget);
});
});
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 108 KiB