feat(client): 财务应收应付汇总增加往来单位搜索(两端,原型同步)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -0,0 +1,194 @@
|
||||
// test/finance_summary_search_test.dart — 财务「应收/应付 汇总」往来单位搜索回归。
|
||||
//
|
||||
// financePartnerRowsProvider(GET /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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 285 KiB |
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 108 KiB |
@@ -105,7 +105,7 @@ AppTokens 目前**仅颜色**。原型还驱动:
|
||||
| 基础数据(移动) | `m-products.html` | 同上窄屏(卡片流) | 同步 | ✅ golden 自比 | ✅ golden 自比 | ✅ golden 自比 |
|
||||
| 入库列表(桌/移) | `stock-in-list.html` | `stock_in_list_screen.dart` | 同步 | ✅ 副标 + 共享 StatusPill(草稿/待审/已审/已拒);**2026-07-14 六项优化**:详情抽屉明细增加生产日期+批次号两列(抽屉加宽 660)、状态筛选改多选 chip 且默认勾选「草稿+待审核」、新建单默认填充默认仓库(原型 stock-in-list.html/stock-in.js 同步);fidelity a 3.323% / b 2.319% / c 3.633%(均 ≤ 阈值 6%),移动 golden 自比全绿 | ✅ golden | ✅ golden |
|
||||
| 出库列表(桌/移) | `stock-out-list.html` | `stock_out_list_screen.dart` | 同步 | ✅ 副标 + 共享 StatusPill;**2026-07-14 六项优化**:详情抽屉明细同增生产日期+批次号两列(抽屉 660)、状态筛选改多选 chip 且默认「草稿+待审核」、新建单默认填充默认仓库(原型 stock-out-list.html 同步);fidelity a 3.228% / b 2.217% / c 3.538%(均 ≤ 阈值 6%),移动 golden 自比全绿 | ✅ golden | ✅ golden |
|
||||
| 财务(桌/移) | `finance.html` | `finance_screen.dart` | 同步 | ✅ **Phase2 重建**:时间范围 chips(真实过滤)+KPI 4 卡(stock summary 环比)+收支趋势柱状图(DsBarChart, /finance/trend)+应收应付汇总(/finance/summary)+流水表+往来抽屉+登记收支(补往来单位);**2026-07-14 六项优化**:流水表筛选新增「往来单位」下拉、时间范围统一改用通用自定义时间组件(原型 finance.html/m-finance.html 同步);fidelity a 4.036% / b 3.131% / c 4.340%(均 ≤ 阈值 6%),移动 golden 自比全绿 | ✅ fidelity | ✅ fidelity |
|
||||
| 财务(桌/移) | `finance.html` | `finance_screen.dart` | 同步 | ✅ **Phase2 重建**:时间范围 chips(真实过滤)+KPI 4 卡(stock summary 环比)+收支趋势柱状图(DsBarChart, /finance/trend)+应收应付汇总(/finance/summary)+流水表+往来抽屉+登记收支(补往来单位);**2026-07-14 六项优化**:流水表筛选新增「往来单位」下拉、时间范围统一改用通用自定义时间组件(原型 finance.html/m-finance.html 同步);fidelity a 4.036% / b 3.131% / c 4.340%(均 ≤ 阈值 6%),移动 golden 自比全绿;**2026-07-14 追加**:应收/应付汇总卡头部新增「往来单位」搜索——本地即时过滤(`/finance/summary` 全量非分页,搜索口径=显示口径),桌面 `DsSearchBox`(`_secHead` trailing)/移动 应收·应付 分段头 `MSearchRow`(流水分段沿用既有 chip→sheet 单选,不重复),原型 finance.html `.sum-search` / m-finance.html 应收应付分段 `.m-search` 同步,finance 相关 6 个整屏 golden + m_finance 3 个 golden 已按新布局 `--update-goldens` | ✅ fidelity | ✅ fidelity |
|
||||
| 设备管理(桌/移) | `devices.html` | `device_management_screen.dart` | 同步 | ✅ **Phase2 重建**:会话表(DsTable)+外设卡网格(custom_fields.peripherals 本地存档)+打印模板;fidelity 2.4–3.0%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
| 系统设置(桌/移) | `settings.html` | `settings_screen.dart` | 同步 | ✅ **Phase2 重建**;假「系统参数」已删;fidelity 1.7–2.3%≤8%;**2026-07-10 授权管理面板迁出**(提升为独立一级屏 `license.html`),subnav 剩 门店信息/用户管理/偏好——**真实端已同步完成**(`settings_screen.dart` 子导航同步摘除授权管理项,golden 已按新态重生成,见授权管理两行) | ✅ fidelity | ✅ fidelity |
|
||||
| 用户管理(桌/移) | `users.html` | `users_screen.dart`(`/settings/users`) | 同步 | ✅ **Phase2 新独立页**:KPI 4 卡+搜索/角色筛选+rcard 弹窗,角色四级拉平;fidelity 1.3–2.1%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
.sec-head{display:flex; align-items:baseline; gap:10px; margin:0 0 12px;}
|
||||
.sec-head h2{margin:0; font-size:var(--fs-h2); color:var(--heading); font-weight:700;}
|
||||
.sec-head .sh-sub{font-size:var(--fs-xs); color:var(--muted);}
|
||||
/* 汇总卡头部往来单位搜索(即时本地过滤,非 combo 单选) */
|
||||
.sec-head .sum-search{margin-left:auto; width:220px; align-self:center;}
|
||||
.block{margin-bottom:22px;}
|
||||
|
||||
/* ====== 新增原子:条形图 .barchart / .bar ====== */
|
||||
@@ -108,7 +110,9 @@
|
||||
|
||||
<!-- 4. 应收/应付 汇总表 -->
|
||||
<div class="block">
|
||||
<div class="sec-head"><h2>应收 / 应付 汇总</h2><span class="sh-sub">按往来单位 · 点击行查看流水</span></div>
|
||||
<div class="sec-head"><h2>应收 / 应付 汇总</h2><span class="sh-sub">按往来单位 · 点击行查看流水</span>
|
||||
<div class="searchbox sum-search"><svg viewBox="0 0 24 24"><use href="#i-search"/></svg><input id="sumSearch" placeholder="搜索往来单位…" oninput="sumKw=this.value; buildSummary();"></div>
|
||||
</div>
|
||||
<div class="table"><table>
|
||||
<thead><tr><th>往来单位</th><th class="num">应收</th><th class="num">应付</th><th class="num">净额</th><th class="act">操作</th></tr></thead>
|
||||
<tbody id="sumBody"></tbody>
|
||||
@@ -181,19 +185,23 @@ const SUMMARY=[
|
||||
{name:'川渝糖酒集团',recv:0,pay:60400}
|
||||
];
|
||||
function yuan(n){ return '¥'+n.toLocaleString(); }
|
||||
// 汇总卡「往来单位」搜索:本地即时过滤(数据已全量加载,非服务端分页,搜索口径=显示口径)
|
||||
let sumKw='';
|
||||
function buildSummary(){
|
||||
document.getElementById('sumBody').innerHTML=SUMMARY.map((s,i)=>{
|
||||
const q=sumKw.trim().toLowerCase();
|
||||
const rows=SUMMARY.map((s,i)=>({...s,i})).filter(s=>!q||s.name.toLowerCase().includes(q));
|
||||
document.getElementById('sumBody').innerHTML=rows.length?rows.map(s=>{
|
||||
const net=s.recv-s.pay;
|
||||
const netCls=net>0?'net-pos':(net<0?'net-neg':'');
|
||||
const netTxt=(net>0?'+':'')+yuan(net);
|
||||
return `<tr class="click" onclick="openPartner(${i})">
|
||||
return `<tr class="click" onclick="openPartner(${s.i})">
|
||||
<td class="pname">${s.name}</td>
|
||||
<td class="num">${s.recv?yuan(s.recv):'—'}</td>
|
||||
<td class="num">${s.pay?yuan(s.pay):'—'}</td>
|
||||
<td class="num ${netCls}">${netTxt}</td>
|
||||
<td class="act" onclick="event.stopPropagation()"><span class="link" onclick="openPartner(${i})">查看</span></td>
|
||||
<td class="act" onclick="event.stopPropagation()"><span class="link" onclick="openPartner(${s.i})">查看</span></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}).join(''):`<tr><td colspan="5"><div class="empty">没有匹配的往来单位</div></td></tr>`;
|
||||
}
|
||||
|
||||
/* ---- 5. 收支流水 ---- */
|
||||
|
||||
@@ -38,6 +38,9 @@
|
||||
<div class="k"><div class="kl">应付合计</div><div class="kv">¥9.6万</div><div class="kd warn">5 笔未结</div></div>
|
||||
</div>
|
||||
<div class="seg" style="width:100%; margin-top:14px"><button class="on" style="flex:1" onclick="setTab('ar')">应收</button><button style="flex:1" onclick="setTab('ap')">应付</button><button style="flex:1" onclick="setTab('flow')">流水</button></div>
|
||||
<!-- 应收/应付分段头部往来单位搜索(同口径本地过滤,仅 ar/ap 显示;流水分段
|
||||
沿用上方时间行「往来单位」chip → sheet 单选,不重复) -->
|
||||
<div class="m-search" id="sumSearchRow" style="margin-top:12px"><svg viewBox="0 0 24 24"><use href="#i-search"/></svg><input id="sumKwInput" placeholder="搜索往来单位…" oninput="sumKw=this.value; render();"></div>
|
||||
<div class="m-section" id="cnt">应收账款</div>
|
||||
<div id="list"></div>
|
||||
</div>
|
||||
@@ -100,15 +103,22 @@ function drawPartnerOpts(kw){
|
||||
}
|
||||
function setFlowParty(v){ flowParty=v; closeSheet(); render(); }
|
||||
|
||||
// 应收/应付分段「往来单位」搜索:本地即时过滤(数据已全量加载,搜索口径=显示口径)
|
||||
let sumKw='';
|
||||
function render(){
|
||||
const cfg={ar:['应收账款',AR],ap:['应付账款',AP],flow:['资金流水',FLOW]}[tab];
|
||||
// 往来单位筛选仅作用于「流水」分段(对应真实端 financeListProvider.setPartner 只过滤流水表)
|
||||
const rows=tab==='flow'?cfg[1].filter(r=>flowParty==='全部'||r.party.split(' · ')[0]===flowParty):cfg[1];
|
||||
let rows=tab==='flow'?cfg[1].filter(r=>flowParty==='全部'||r.party.split(' · ')[0]===flowParty):cfg[1];
|
||||
document.getElementById('sumSearchRow').style.display=tab==='flow'?'none':'flex';
|
||||
if(tab!=='flow'){
|
||||
const q=sumKw.trim().toLowerCase();
|
||||
if(q) rows=rows.filter(r=>r.party.toLowerCase().includes(q));
|
||||
}
|
||||
document.getElementById('cnt').textContent=`${cfg[0]} · 共 ${rows.length}`;
|
||||
document.getElementById('list').innerHTML=rows.map(r=>`<div class="m-card"><div class="m-row">
|
||||
document.getElementById('list').innerHTML=rows.length?rows.map(r=>`<div class="m-card"><div class="m-row">
|
||||
<div class="mc-main"><div class="mc-nm">${r.party}</div><div class="mc-sub">${r.date}</div></div>
|
||||
<div class="mc-right"><span class="badge b-${r.status}">${r.status}</span><span class="mc-amt">${r.amt}</span></div>
|
||||
</div></div>`).join('');
|
||||
</div></div>`).join(''):`<div class="m-empty"><svg viewBox="0 0 24 24"><use href="#i-box"/></svg>${tab==='flow'?'暂无数据':'没有匹配的往来单位'}</div>`;
|
||||
document.getElementById('partyVal').textContent=flowParty==='全部'?'':flowParty;
|
||||
document.getElementById('chipParty').classList.toggle('on',flowParty!=='全部');
|
||||
}
|
||||
|
||||