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 = ''; String _summaryKw = '';
// 桌面汇总表表头「往来单位」下拉筛选(验收反馈改版:由卡标题行文字搜索 // 桌面汇总表表头「往来单位」下拉筛选(验收反馈改版:由卡标题行文字搜索
// 改为表头内嵌 ComboSearchField,同 _flowTable 往来单位筛选控件;单选精确 // 改为表头内嵌 ComboSearchField,同 _flowTable 往来单位筛选控件;单选精确
// 匹配一行,× 清除恢复全量;候选=当前汇总行往来单位,口径=显示口径)。 // 匹配一行,× 清除恢复全量;候选=全量往来单位(allPartnersProvider,与
// _flowTable toolbar 同源),选中一个汇总里没有行的单位时显示空态)。
int? _summaryPartnerId; int? _summaryPartnerId;
static const _flowChips = ['全部', '应收', '应付', '收款', '付款']; static const _flowChips = ['全部', '应收', '应付', '收款', '付款'];
@@ -111,8 +112,7 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
case '自定义': case '自定义':
final picked = await showDateRangeDropdown( final picked = await showDateRangeDropdown(
anchorContext, anchorContext,
initial: initial: _customRange ??
_customRange ??
DateTimeRange(start: DateTime(now.year, now.month, 1), end: now), DateTimeRange(start: DateTime(now.year, now.month, 1), end: now),
); );
if (picked == null) return; if (picked == null) return;
@@ -575,15 +575,15 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
final t = context.tokens; final t = context.tokens;
final trend = ref.watch(financeTrendProvider); final trend = ref.watch(financeTrendProvider);
Widget placeholder(Widget child) => Container( Widget placeholder(Widget child) => Container(
height: 120, height: 120,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: t.surface, color: t.surface,
border: Border.all(color: t.border), border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rLg), borderRadius: BorderRadius.circular(AppDims.rLg),
), ),
child: child, child: child,
); );
return trend.when( return trend.when(
loading: () => placeholder( loading: () => placeholder(
const SizedBox( const SizedBox(
@@ -640,19 +640,17 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
), ),
data: (allRows) { data: (allRows) {
final rows = _filterSummaryByPartner(allRows); final rows = _filterSummaryByPartner(allRows);
// 下拉候选=当前汇总行往来单位(口径=显示口径,不用全量 partners,避免 // 下拉候选=全量往来单位(验收反馈:与 _flowTable toolbar 往来单位筛选
// 选到无余额单位出空表);id 合成规则同 _filterSummaryByPartner。 // 同源 allPartnersProvider,而非只列汇总行里出现的单位);选中一个汇总
final partnerOptions = <OptionItem>[ // 无余额的单位时,_filterSummaryByPartner 匹配不到行会返回空列表,
for (var i = 0; i < allRows.length; i++) // 走既有「没有匹配的往来单位」空态。
OptionItem( final partnerOptions =
id: allRows[i].partnerId ?? -(i + 1), (ref.watch(allPartnersProvider).valueOrNull?.data ?? const [])
name: allRows[i].name, .map((p) => OptionItem(id: p.id, name: p.name, code: p.code))
), .toList();
];
return DsTable( return DsTable(
shrinkWrap: true, shrinkWrap: true,
emptyText: emptyText: _summaryPartnerId == null ? '暂无未结清的应收 / 应付' : '没有匹配的往来单位',
_summaryPartnerId == null ? '暂无未结清的应收 / 应付' : '没有匹配的往来单位',
mobileCards: rows mobileCards: rows
.map( .map(
(r) => MobileListCard( (r) => MobileListCard(
@@ -682,26 +680,35 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
'往来单位', '往来单位',
// 表头「往来单位」文字右边内嵌下拉(验收反馈:由卡标题行搜索框 // 表头「往来单位」文字右边内嵌下拉(验收反馈:由卡标题行搜索框
// 改到此处,与 _flowTable 往来单位筛选同款 ComboSearchField)。 // 改到此处,与 _flowTable 往来单位筛选同款 ComboSearchField)。
header: Row( // 外包 SizedBox 固定高=ComboSearchField 自身高度:Row 的
mainAxisSize: MainAxisSize.min, // crossAxisAlignment.center 依赖两个子项各自的内在高度居中,
children: [ // 标签文字行高在不同字体下可能与 34 有偏差,导致下拉在表头行
Text( // 里视觉偏上(验收反馈);显式钉住 Row 高度消除该偏差,不改
'往来单位', // 表头行 pad 11/14 的既有行高口径。
style: TextStyle( header: SizedBox(
fontSize: AppDims.fsSm, height: 34,
fontWeight: FontWeight.w600, child: Row(
color: t.muted, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'往来单位',
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600,
color: t.muted,
),
), ),
), const SizedBox(width: 10),
const SizedBox(width: 10), ComboSearchField(
ComboSearchField( options: partnerOptions,
options: partnerOptions, selectedId: _summaryPartnerId,
selectedId: _summaryPartnerId, hint: '往来单位',
hint: '搜索往来单位', width: 180,
width: 180, onChanged: _setSummaryPartner,
onChanged: _setSummaryPartner, ),
), ],
], ),
), ),
), ),
const DsColumn('recv', '应收', numeric: true), const DsColumn('recv', '应收', numeric: true),
@@ -740,9 +747,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
fontFamily: AppFonts.mono, fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback, fontFamilyFallback: AppFonts.monoFallback,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: r.net > 0 color:
? t.success r.net > 0 ? t.success : (r.net < 0 ? t.danger : t.text),
: (r.net < 0 ? t.danger : t.text),
), ),
), ),
InkWell( InkWell(
@@ -806,14 +812,12 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
// stock_out_list_screen.dart 客户 ComboSearchField 写法)。仅桌面 // stock_out_list_screen.dart 客户 ComboSearchField 写法)。仅桌面
// 工具栏放置,移动端对应筛选在 _timeRow 尾部的「往来单位」chip → sheet。 // 工具栏放置,移动端对应筛选在 _timeRow 尾部的「往来单位」chip → sheet。
ComboSearchField( ComboSearchField(
options: options: (ref.watch(allPartnersProvider).valueOrNull?.data ??
(ref.watch(allPartnersProvider).valueOrNull?.data ?? const [])
const []) .map(
.map( (p) => OptionItem(id: p.id, name: p.name, code: p.code),
(p) => )
OptionItem(id: p.id, name: p.name, code: p.code), .toList(),
)
.toList(),
selectedId: _partnerId, selectedId: _partnerId,
hint: '往来单位', hint: '往来单位',
width: 180, width: 180,
@@ -971,8 +975,7 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
MobileCardField('状态', r.status == 'open' ? '未结清' : '已结清'), MobileCardField('状态', r.status == 'open' ? '未结清' : '已结清'),
if (r.remark?.isNotEmpty == true) MobileCardField('备注', r.remark), if (r.remark?.isNotEmpty == true) MobileCardField('备注', r.remark),
], ],
actions: actions: (r.status == 'open' &&
(r.status == 'open' &&
(r.type == 'receivable' || r.type == 'payable')) (r.type == 'receivable' || r.type == 'payable'))
? [ ? [
WriteGuard( WriteGuard(
@@ -990,23 +993,22 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
/// delta 箭头/色调映射(与 DsKpi 同规则:▲/▼ 文本字符)。 /// delta 箭头/色调映射(与 DsKpi 同规则:▲/▼ 文本字符)。
String _mDeltaText(String text, DsKpiDelta tone) => switch (tone) { String _mDeltaText(String text, DsKpiDelta tone) => switch (tone) {
DsKpiDelta.up => '$text', DsKpiDelta.up => '$text',
DsKpiDelta.down => '$text', DsKpiDelta.down => '$text',
DsKpiDelta.neutral => text, DsKpiDelta.neutral => text,
}; };
MKpiDeltaTone _mTone(DsKpiDelta tone) => switch (tone) { MKpiDeltaTone _mTone(DsKpiDelta tone) => switch (tone) {
DsKpiDelta.up => MKpiDeltaTone.up, DsKpiDelta.up => MKpiDeltaTone.up,
DsKpiDelta.down => MKpiDeltaTone.down, DsKpiDelta.down => MKpiDeltaTone.down,
DsKpiDelta.neutral => MKpiDeltaTone.normal, DsKpiDelta.neutral => MKpiDeltaTone.normal,
}; };
/// 窄屏 KPI 2×2(原型 .m-kpi:本月收入 / 本月支出 / 应收合计 / 应付合计)。 /// 窄屏 KPI 2×2(原型 .m-kpi:本月收入 / 本月支出 / 应收合计 / 应付合计)。
Widget _mKpis(dynamic t) { Widget _mKpis(dynamic t) {
final outSum = ref.watch(stockOutSummaryProvider).valueOrNull; final outSum = ref.watch(stockOutSummaryProvider).valueOrNull;
final inSum = ref.watch(stockInSummaryProvider).valueOrNull; final inSum = ref.watch(stockInSummaryProvider).valueOrNull;
final rows = final rows = ref.watch(financePartnerRowsProvider).valueOrNull ??
ref.watch(financePartnerRowsProvider).valueOrNull ??
const <PartnerFinanceRow>[]; const <PartnerFinanceRow>[];
var recv = 0.0, pay = 0.0, openCount = 0; var recv = 0.0, pay = 0.0, openCount = 0;
for (final r in rows) { for (final r in rows) {
@@ -1054,32 +1056,32 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
/// 三分段列表体:m-section 计数标题 + 卡片流(应收/应付=往来汇总,流水=收支记录)。 /// 三分段列表体:m-section 计数标题 + 卡片流(应收/应付=往来汇总,流水=收支记录)。
List<Widget> _mSegBody(dynamic t, List<FinanceRecord> flows) { List<Widget> _mSegBody(dynamic t, List<FinanceRecord> flows) {
Widget section(String title, int count) => Padding( Widget section(String title, int count) => Padding(
padding: const EdgeInsets.fromLTRB(2, 16, 2, 8), padding: const EdgeInsets.fromLTRB(2, 16, 2, 8),
child: Text( child: Text(
'$title · 共 $count', '$title · 共 $count',
style: TextStyle( style: TextStyle(
fontSize: AppDims.fsSm, fontSize: AppDims.fsSm,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
letterSpacing: .4, letterSpacing: .4,
color: t.muted, color: t.muted,
), ),
), ),
); );
Widget empty([String msg = '暂无数据']) => Padding( Widget empty([String msg = '暂无数据']) => Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: Center( child: Center(
child: Text( child: Text(
msg, msg,
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm), style: TextStyle(color: t.muted, fontSize: AppDims.fsSm),
), ),
), ),
); );
List<Widget> stacked(List<Widget> cards) => [ List<Widget> stacked(List<Widget> cards) => [
for (var i = 0; i < cards.length; i++) ...[ for (var i = 0; i < cards.length; i++) ...[
if (i > 0) const SizedBox(height: 10), if (i > 0) const SizedBox(height: 10),
cards[i], cards[i],
], ],
]; ];
if (_mSeg == 2) { if (_mSeg == 2) {
return [ return [
@@ -1216,34 +1218,35 @@ class _PartnerSheetBodyState extends State<_PartnerSheetBody> {
required String name, required String name,
required bool selected, required bool selected,
required VoidCallback onTap, required VoidCallback onTap,
}) => InkWell( }) =>
onTap: onTap, InkWell(
child: Container( onTap: onTap,
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 4), child: Container(
decoration: BoxDecoration( padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 4),
border: Border(bottom: BorderSide(color: t.borderSubtle)), 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) ...[ child: Row(
const SizedBox(width: 8), children: [
Icon(LucideIcons.check, size: 18, color: t.primary), 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( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
+40 -12
View File
@@ -77,6 +77,9 @@ class _FakeFinanceRepository extends FinanceRepository {
Future<List<TrendPoint>> trend({int months = 6}) async => const []; Future<List<TrendPoint>> trend({int months = 6}) async => const [];
} }
/// 第 4 条「未挂账供应商」故意不出现在 partnerSummary() 里,用于验证下拉
/// 候选=全量往来单位(而非只列汇总行里出现的单位):选中它后汇总应展示
/// 既有「没有匹配的往来单位」空态。
class _FakeAllPartners extends PartnerListNotifier { class _FakeAllPartners extends PartnerListNotifier {
@override @override
Future<PageResult<Partner>> build() async => const PageResult( Future<PageResult<Partner>> build() async => const PageResult(
@@ -84,8 +87,9 @@ class _FakeAllPartners extends PartnerListNotifier {
Partner(id: 1, name: '华东酒类批发', type: 'customer'), Partner(id: 1, name: '华东酒类批发', type: 'customer'),
Partner(id: 2, name: '金樽商贸', type: 'customer'), Partner(id: 2, name: '金樽商贸', type: 'customer'),
Partner(id: 3, name: '茅台华南经销', type: 'supplier'), Partner(id: 3, name: '茅台华南经销', type: 'supplier'),
Partner(id: 4, name: '未挂账供应商', type: 'supplier'),
], ],
total: 3, total: 4,
page: 1, page: 1,
pageSize: 1000, pageSize: 1000,
); );
@@ -94,10 +98,10 @@ class _FakeAllPartners extends PartnerListNotifier {
List<Override> _overrides() => [ List<Override> _overrides() => [
financeRepositoryProvider.overrideWith((ref) => _FakeFinanceRepository()), financeRepositoryProvider.overrideWith((ref) => _FakeFinanceRepository()),
allPartnersProvider.overrideWith(() => _FakeAllPartners()), allPartnersProvider.overrideWith(() => _FakeAllPartners()),
stockOutSummaryProvider.overrideWith( stockOutSummaryProvider.overrideWith((ref) async =>
(ref) async => const StockSummary(monthAmount: 0, lastMonthAmount: 0)), const StockSummary(monthAmount: 0, lastMonthAmount: 0)),
stockInSummaryProvider.overrideWith( stockInSummaryProvider.overrideWith((ref) async =>
(ref) async => const StockSummary(monthAmount: 0, lastMonthAmount: 0)), const StockSummary(monthAmount: 0, lastMonthAmount: 0)),
isReadonlyProvider.overrideWithValue(false), isReadonlyProvider.overrideWithValue(false),
]; ];
@@ -134,13 +138,13 @@ void main() {
expect(find.text('金樽商贸'), findsOneWidget); expect(find.text('金樽商贸'), findsOneWidget);
expect(find.text('茅台华南经销'), findsOneWidget); expect(find.text('茅台华南经销'), findsOneWidget);
// 定位表头内嵌的 ComboSearchField(按构造参数 hint 匹配,避免 // 定位表头内嵌的 ComboSearchField:验收反馈后候选/占位文案
// _flowTable toolbar 的往来单位 ComboSearchField(hint:'往来单位') // _flowTable toolbar 的往来单位 ComboSearchField 完全同款(同为
// 混淆;选中后触发器文案会变成往来单位名,故不能用显示文字定位)。 // hint:'往来单位'),不能再靠 hint/显示文字区分;改用构建顺序——
final combo = find.byWidgetPredicate( // 汇总表(_summaryTable)先于流水表(_flowTable)出现在页面里,
(w) => w is ComboSearchField && w.hint == '搜索往来单位', // 故 ComboSearchField 的第一个即汇总表头这个。
); final combo = find.byType(ComboSearchField).first;
expect(combo, findsOneWidget); expect(find.byType(ComboSearchField), findsNWidgets(2));
await tester.ensureVisible(combo); await tester.ensureVisible(combo);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@@ -168,6 +172,30 @@ void main() {
expect(find.text('金樽商贸'), findsOneWidget); expect(find.text('金樽商贸'), findsOneWidget);
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('移动应收/应付分段往来单位搜索', () { 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

+1 -1
View File
@@ -105,7 +105,7 @@ AppTokens 目前**仅颜色**。原型还驱动:
| 基础数据(移动) | `m-products.html` | 同上窄屏(卡片流) | 同步 | ✅ golden 自比 | ✅ golden 自比 | ✅ golden 自比 | | 基础数据(移动) | `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-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 | | 出库列表(桌/移) | `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 自比全绿;**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`**2026-07-14 验收反馈调整**:桌面「往来单位」搜索由卡标题行 `DsSearchBox` 挪进汇总表表头「往来单位」列头右侧,改为下拉单选 `ComboSearchField`(同 `_flowTable` 往来单位筛选控件,选中精确匹配一行/× 清除恢复全量,候选=当前汇总行往来单位);原型 `.sec-head .sum-search` 移除,改 `.th-party-wrap` 内嵌 `.combo` + `openSearchMenu` 弹层;移动端未动。desktop `finance` golden 3 主题已 `--update-goldens`3.11% 差异,目检确认新版式),`finance_mobile`/`m_finance` 无变化、原样通过 | ✅ 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`**2026-07-14 验收反馈调整**:桌面「往来单位」搜索由卡标题行 `DsSearchBox` 挪进汇总表表头「往来单位」列头右侧,改为下拉单选 `ComboSearchField`(同 `_flowTable` 往来单位筛选控件,选中精确匹配一行/× 清除恢复全量,候选=当前汇总行往来单位);原型 `.sec-head .sum-search` 移除,改 `.th-party-wrap` 内嵌 `.combo` + `openSearchMenu` 弹层;移动端未动。desktop `finance` golden 3 主题已 `--update-goldens`3.11% 差异,目检确认新版式),`finance_mobile`/`m_finance` 无变化、原样通过;**2026-07-14 验收反馈2**(实机截图 3 处细节):①下拉候选由「仅当前汇总行往来单位」改为「全量往来单位」(`allPartnersProvider`,与 `_flowTable` toolbar 同源),选中一个汇总没有余额行的单位时走既有「没有匹配的往来单位」空态;②占位符叠词修复——`ComboSearchField.hint``'搜索往来单位'``'往来单位'`(内部会自动拼「搜索」前缀),弹层内搜索框恢复正常显示「搜索往来单位…」,收起态触发器文案对齐 `_flowTable` 写法同为「往来单位」;③表头行下拉垂直位置——`header` 外包 `SizedBox(height: 34)` 显式钉住 Row 高度消除潜在字体行高偏差,不改表头行 pad 11/14 既有行高口径。原型 finance.html 同步:`ALL_PARTNERS``SUMMARY` 全部单位 + 1 个无余额行的「鼎丰超市」演示空态)替换 `sumPartyId`/`SUMMARY` 候选、`#sumPartyInput` 收起态 placeholder 改「往来单位」;`.th-party-wrap{align-items:center}` + 浏览器 `th` 默认 `vertical-align:middle` 本就居中,未改 CSS。desktop `finance` golden 3 主题已 `--update-goldens`(0.04% 差异,目检确认新候选/占位文案,表头对齐无回归),`finance_mobile`/`m_finance` 无变化、原样通过 | ✅ fidelity | ✅ fidelity |
| 设备管理(桌/移) | `devices.html` | `device_management_screen.dart` | 同步 | ✅ **Phase2 重建**:会话表(DsTable)+外设卡网格(custom_fields.peripherals 本地存档)+打印模板;fidelity 2.43.0%≤8% | ✅ fidelity | ✅ fidelity | | 设备管理(桌/移) | `devices.html` | `device_management_screen.dart` | 同步 | ✅ **Phase2 重建**:会话表(DsTable)+外设卡网格(custom_fields.peripherals 本地存档)+打印模板;fidelity 2.43.0%≤8% | ✅ fidelity | ✅ fidelity |
| 系统设置(桌/移) | `settings.html` | `settings_screen.dart` | 同步 | ✅ **Phase2 重建**;假「系统参数」已删;fidelity 1.72.3%≤8%**2026-07-10 授权管理面板迁出**(提升为独立一级屏 `license.html`),subnav 剩 门店信息/用户管理/偏好——**真实端已同步完成**(`settings_screen.dart` 子导航同步摘除授权管理项,golden 已按新态重生成,见授权管理两行) | ✅ fidelity | ✅ fidelity | | 系统设置(桌/移) | `settings.html` | `settings_screen.dart` | 同步 | ✅ **Phase2 重建**;假「系统参数」已删;fidelity 1.72.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.32.1%≤8% | ✅ fidelity | ✅ fidelity | | 用户管理(桌/移) | `users.html` | `users_screen.dart``/settings/users` | 同步 | ✅ **Phase2 新独立页**KPI 4 卡+搜索/角色筛选+rcard 弹窗,角色四级拉平;fidelity 1.32.1%≤8% | ✅ fidelity | ✅ fidelity |
+14 -10
View File
@@ -115,7 +115,7 @@
<div class="sec-head"><h2>应收 / 应付 汇总</h2><span class="sh-sub">按往来单位 · 点击行查看流水</span></div> <div class="sec-head"><h2>应收 / 应付 汇总</h2><span class="sh-sub">按往来单位 · 点击行查看流水</span></div>
<div class="table"><table> <div class="table"><table>
<thead><tr> <thead><tr>
<th><div class="th-party-wrap"><span>往来单位</span><div class="combo" id="sumPartyCombo" onclick="openSumPartyFilter(this)"><input id="sumPartyInput" readonly placeholder="搜索往来单位"><span class="caf" id="sumPartyCaf" onclick="sumPartyCafClick(event)"><svg viewBox="0 0 24 24" stroke-width="2"><use href="#i-chevron-down"/></svg></span></div></div></th> <th><div class="th-party-wrap"><span>往来单位</span><div class="combo" id="sumPartyCombo" onclick="openSumPartyFilter(this)"><input id="sumPartyInput" readonly placeholder="往来单位"><span class="caf" id="sumPartyCaf" onclick="sumPartyCafClick(event)"><svg viewBox="0 0 24 24" stroke-width="2"><use href="#i-chevron-down"/></svg></span></div></div></th>
<th class="num">应收</th><th class="num">应付</th><th class="num">净额</th><th class="act">操作</th> <th class="num">应收</th><th class="num">应付</th><th class="num">净额</th><th class="act">操作</th>
</tr></thead> </tr></thead>
<tbody id="sumBody"></tbody> <tbody id="sumBody"></tbody>
@@ -189,23 +189,27 @@ const SUMMARY=[
]; ];
function yuan(n){ return '¥'+n.toLocaleString(); } function yuan(n){ return '¥'+n.toLocaleString(); }
// 汇总表头「往来单位」下拉筛选(对齐 ComboSearchField:单选精确匹配一行,非 // 汇总表头「往来单位」下拉筛选(对齐 ComboSearchField:单选精确匹配一行,非
// 关键字子串过滤;候选=当前汇总行往来单位,搜索口径=显示口径,不用全量往来 // 关键字子串过滤;验收反馈改版:候选=全量往来单位(与收支流水 toolbar 往来
// 单位库,避免选到无余额单位出空表) // 单位筛选同源),而非只列当前汇总行里出现的单位——选中一个汇总没有余额
let sumPartyId=null; // 选中的 SUMMARY 下标;null=全部 // 行的单位时,走既有「没有匹配的往来单位」空态)。
// ALL_PARTNERS 模拟真实 allPartnersProvider 全量库:含 SUMMARY 全部单位 +
// 1 个汇总里没有余额行的「鼎丰超市」,用于还原空态场景。
const ALL_PARTNERS=[...SUMMARY.map(s=>s.name),'鼎丰超市'];
let sumPartyName=null; // 选中的往来单位名;null=全部
function openSumPartyFilter(t){ function openSumPartyFilter(t){
openSearchMenu(t, SUMMARY.map((s,i)=>({v:i,label:s.name,sel:i===sumPartyId})), v=>{ openSearchMenu(t, ALL_PARTNERS.map(name=>({v:name,label:name,sel:name===sumPartyName})), v=>{
sumPartyId=Number(v); closeMenus(); buildSummary(); sumPartyName=v; closeMenus(); buildSummary();
}, {placeholder:'搜索往来单位…'}); }, {placeholder:'搜索往来单位…'});
} }
// combo 已选中值(.has)时,点击右侧图标(此时为 ×)清除,不弹出下拉 // combo 已选中值(.has)时,点击右侧图标(此时为 ×)清除,不弹出下拉
function sumPartyCafClick(e){ function sumPartyCafClick(e){
if(sumPartyId!=null){ e.stopPropagation(); sumPartyId=null; buildSummary(); } if(sumPartyName!=null){ e.stopPropagation(); sumPartyName=null; buildSummary(); }
} }
function buildSummary(){ function buildSummary(){
const rows=sumPartyId==null?SUMMARY.map((s,i)=>({...s,i})):SUMMARY.map((s,i)=>({...s,i})).filter(s=>s.i===sumPartyId); const rows=sumPartyName==null?SUMMARY.map((s,i)=>({...s,i})):SUMMARY.map((s,i)=>({...s,i})).filter(s=>s.name===sumPartyName);
const has=sumPartyId!=null; const has=sumPartyName!=null;
document.getElementById('sumPartyCombo').classList.toggle('has',has); document.getElementById('sumPartyCombo').classList.toggle('has',has);
document.getElementById('sumPartyInput').value=has?SUMMARY[sumPartyId].name:''; document.getElementById('sumPartyInput').value=has?sumPartyName:'';
document.getElementById('sumPartyCaf').innerHTML=has document.getElementById('sumPartyCaf').innerHTML=has
?'<svg viewBox="0 0 24 24" stroke-width="2"><use href="#i-close"/></svg>' ?'<svg viewBox="0 0 24 24" stroke-width="2"><use href="#i-close"/></svg>'
:'<svg viewBox="0 0 24 24" stroke-width="2"><use href="#i-chevron-down"/></svg>'; :'<svg viewBox="0 0 24 24" stroke-width="2"><use href="#i-chevron-down"/></svg>';