feat(client): 库存/盘点/财务窄屏对齐移动原型(KPI 网格/卡片流/图标徽章/sheet)

- 库存列表:窄屏隐藏页内大标题头;KPI 2×2 MKpiGrid(缺货卡 warn+点击筛选);
  MSearchRow(详搜钮开筛选 sheet:状态/规格/系列);卡片徽章换图标变体
- 库存盘点:窄屏卡片流(单号/范围·项数/进行中徽章带图标/差异/日期)+
  详情 sheet(drow 键值行)+ 底部操作条;DateTime.now→appNow 可注入时钟
- 财务:窄屏隐藏大标题头;KPI 2×2(收入 up 绿/支出 down 红/应收 warn);
  DsSeg 应收/应付/流水三分段 + 卡片流(类型徽章带图标、金额正负色)
- golden:新增 m_inventory / m_inventory_check / m_finance 三屏×三主题
  (390×844@2x);重生成既有 inventory_list_mobile / finance_mobile 基准
  (桌面 golden 零改动)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-04 12:18:17 +08:00
parent ff7b4194ec
commit dde87a32b6
21 changed files with 1055 additions and 117 deletions
@@ -0,0 +1,126 @@
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/auth/auth_state.dart';
import 'package:jiu_client/core/models/page_result.dart';
import 'package:jiu_client/core/utils/clock.dart';
import 'package:jiu_client/models/finance.dart';
import 'package:jiu_client/models/stock_summary.dart';
import 'package:jiu_client/providers/finance_provider.dart';
import 'package:jiu_client/providers/stock_in_provider.dart';
import 'package:jiu_client/providers/stock_out_provider.dart';
import 'package:jiu_client/screens/finance/finance_screen.dart';
import '../support/golden_harness.dart';
/// 财务管理 窄屏形态 golden × 三主题(390×844 @2x)——对齐原型 m-finance.html
/// 隐藏页内大标题头 + KPI 2×2(收入 up 绿 / 支出 down 红)+ DsSeg 三分段
/// + 应收卡片流(往来单位 + 金额 + 未结清徽章带图标)。fixtures 与桌面 golden 同套。
/// 更新基准:flutter test --update-goldens test/golden/finance_mobile_golden_test.dart
// 原型 SUMMARY 8 行
const _rows = [
PartnerFinanceRow(partnerId: 1, name: '华东酒类批发', recv: 128400, openCount: 2),
PartnerFinanceRow(partnerId: 2, name: '金樽商贸', recv: 86200, openCount: 1),
PartnerFinanceRow(partnerId: 3, name: '茅台华南经销', pay: 142800, openCount: 1),
PartnerFinanceRow(partnerId: 4, name: '鸿运餐饮连锁', recv: 74600, openCount: 1),
PartnerFinanceRow(partnerId: 5, name: '五粮液省级代理', pay: 96400, openCount: 1),
PartnerFinanceRow(
partnerId: 6, name: '盛世名酒城', recv: 52300, pay: 18600, openCount: 2),
PartnerFinanceRow(partnerId: 7, name: '御品烟酒行', recv: 31800, openCount: 1),
PartnerFinanceRow(partnerId: 8, name: '川渝糖酒集团', pay: 60400, openCount: 1),
];
// 原型 FLOWS 前 5 条(流水分段用;390×844 首屏展示应收卡片流)
const _flows = [
FinanceRecord(
id: 1,
type: 'receipt',
amount: 68000,
balance: 0,
status: 'closed',
partnerName: '华东酒类批发',
recordDate: '2026-06-20',
remark: '飞天茅台货款'),
FinanceRecord(
id: 2,
type: 'payment',
amount: 142800,
balance: 0,
status: 'closed',
partnerName: '茅台华南经销',
recordDate: '2026-06-19',
remark: '6月采购结算'),
FinanceRecord(
id: 3,
type: 'receipt',
amount: 42600,
balance: 0,
status: 'closed',
partnerName: '金樽商贸',
recordDate: '2026-06-18',
remark: '五粮液尾款'),
FinanceRecord(
id: 4,
type: 'receipt',
amount: 31500,
balance: 0,
status: 'closed',
partnerName: '鸿运餐饮连锁',
recordDate: '2026-06-17',
remark: '宴会用酒'),
FinanceRecord(
id: 5,
type: 'payment',
amount: 96400,
balance: 0,
status: 'closed',
partnerName: '五粮液省级代理',
recordDate: '2026-06-16',
remark: '普五补货'),
];
class _FakeFinanceList extends FinanceListNotifier {
@override
Future<PageResult<FinanceRecord>> build() async =>
const PageResult(data: _flows, total: 5, page: 1, pageSize: 50);
@override
void setType(String type) {}
@override
void setMonth(String month) {}
@override
void setRange(String startDate, String endDate) {}
@override
void setPage(int page) {}
@override
void setPageSize(int pageSize) {}
@override
void reload() {}
}
List<Override> _overrides() => [
financeListProvider.overrideWith(() => _FakeFinanceList()),
financePartnerRowsProvider.overrideWith((ref) async => _rows),
// 环比凑成原型 ▲8.4% / ▼3.1%
stockOutSummaryProvider.overrideWith((ref) async =>
const StockSummary(monthAmount: 1860000, lastMonthAmount: 1715867)),
stockInSummaryProvider.overrideWith((ref) async =>
const StockSummary(monthAmount: 1320000, lastMonthAmount: 1362229)),
isReadonlyProvider.overrideWithValue(false),
];
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
appClock = () => DateTime(2026, 7, 13, 9, 30, 15);
goldenAcrossThemes(
'finance 窄屏(m-finance',
goldenPrefix: 'm_finance',
child: () => const Scaffold(body: FinanceScreen()),
overrides: _overrides,
logical: const Size(390, 844),
dpr: 2,
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jiu_client/core/utils/clock.dart';
import 'package:jiu_client/models/warehouse.dart';
import 'package:jiu_client/providers/warehouse_provider.dart';
import 'package:jiu_client/screens/inventory/inventory_check_screen.dart';
import '../support/golden_harness.dart';
/// 库存盘点 窄屏形态 golden × 三主题(390×844 @2x)——对齐原型 m-inventory-check.html
/// 粒度:基本信息卡 + 「盘点单」卡片流(单号/范围·项数/进行中徽章带图标/差异/日期)
/// + 底部操作条(取消/提交盘点)。时钟冻结保证单号/日期确定化。
/// 更新基准:flutter test --update-goldens test/golden/inventory_check_mobile_golden_test.dart
const _warehouses = [
Warehouse(id: 1, name: '主仓', isDefault: true),
Warehouse(id: 2, name: '东库', isDefault: false),
];
class _FakeWarehouses extends WarehouseListNotifier {
@override
Future<List<Warehouse>> build() async => _warehouses;
}
List<Override> _overrides() => [
warehouseListProvider.overrideWith(() => _FakeWarehouses()),
];
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
appClock = () => DateTime(2026, 6, 20, 9, 30, 15);
goldenAcrossThemes(
'inventory check 窄屏(m-inventory-check',
goldenPrefix: 'm_inventory_check',
child: () => const InventoryCheckScreen(),
overrides: _overrides,
logical: const Size(390, 844),
dpr: 2,
);
}
@@ -0,0 +1,192 @@
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/models/page_result.dart';
import 'package:jiu_client/core/auth/auth_state.dart';
import 'package:jiu_client/models/inventory.dart';
import 'package:jiu_client/models/product_option.dart';
import 'package:jiu_client/providers/inventory_provider.dart';
import 'package:jiu_client/providers/product_option_provider.dart';
import 'package:jiu_client/screens/inventory/inventory_list_screen.dart';
import '../support/golden_harness.dart';
/// 库存列表 窄屏形态 golden × 三主题(390×844 @2x)——对齐原型 m-inventory.html
/// 隐藏页内大标题头 + KPI 2×2 MKpiGrid(缺货卡 warn+ MSearchRow + 卡片流图标徽章。
/// 更新基准:flutter test --update-goldens test/golden/inventory_list_mobile_golden_test.dart
// 原型 ITEMS 前 10 行(与桌面 golden 同套 fixtures)。
const _items = [
Inventory(
id: 1,
productId: 1,
quantity: 128,
productCode: 'MT-FT-500',
productName: '茅台 飞天 53°',
series: '飞天',
spec: '500ml×6',
unit: '',
warehouseName: '主仓',
unitPrice: 2680,
salePrice: 2680,
productionDate: '2024-07-11',
batchNo: 'PZ240711',
supplierName: '贵州茅台经销',
minStock: 10,
createdAt: '2026-06-14',
),
Inventory(
id: 2,
productId: 2,
quantity: 64,
productCode: 'WLY-PW-500',
productName: '五粮液 普五 52°',
series: '普五',
spec: '500ml×6',
unit: '',
warehouseName: '主仓',
unitPrice: 1050,
salePrice: 1050,
productionDate: '2024-04-28',
batchNo: 'PZ240428',
supplierName: '宜宾五粮液',
minStock: 10,
createdAt: '2026-06-14',
),
Inventory(
id: 3,
productId: 3,
quantity: 8,
productCode: 'JNC-SJ-500',
productName: '剑南春 水晶剑',
series: '水晶剑',
spec: '500ml×6',
unit: '',
warehouseName: '主仓',
unitPrice: 438,
salePrice: 438,
productionDate: '2023-11-15',
batchNo: 'PZ231115',
supplierName: '绵竹剑南春',
minStock: 10,
createdAt: '2026-06-12',
),
Inventory(
id: 8,
productId: 8,
quantity: 0,
productCode: 'FJ-QH20-500',
productName: '汾酒 青花 20',
series: '青花20',
spec: '500ml×6',
unit: '',
warehouseName: '主仓',
unitPrice: 420,
salePrice: 420,
productionDate: '2024-01-12',
batchNo: 'PZ240112',
supplierName: '山西杏花村',
minStock: 10,
createdAt: '2026-06-03',
),
Inventory(
id: 4,
productId: 4,
quantity: 42,
productCode: 'GJ-1573-500',
productName: '国窖 1573',
series: '1573',
spec: '500ml×6',
unit: '',
warehouseName: '主仓',
unitPrice: 980,
salePrice: 980,
productionDate: '2024-02-08',
batchNo: 'PZ240208',
supplierName: '泸州老窖',
minStock: 10,
createdAt: '2026-06-10',
),
Inventory(
id: 5,
productId: 5,
quantity: 23,
productCode: 'YH-M6-500',
productName: '洋河 梦之蓝 M6',
series: '梦之蓝M6',
spec: '550ml×6',
unit: '',
warehouseName: '主仓',
unitPrice: 620,
salePrice: 620,
productionDate: '2024-05-20',
batchNo: 'PZ240520',
supplierName: '洋河股份',
minStock: 30,
createdAt: '2026-06-09',
),
];
class _FakeInvNotifier extends InventoryListNotifier {
@override
Future<PageResult<Inventory>> build() async =>
const PageResult(data: _items, total: 6, page: 1, pageSize: 10);
@override
void setPage(int page) {}
@override
void setPageSize(int pageSize) {}
@override
void setWarehouseId(int? id) {}
@override
void setKeyword(String keyword) {}
@override
void setCode(String code) {}
@override
void setSeries(List<String> series) {}
@override
void setSpec(List<String> spec) {}
@override
void reload() {}
}
class _FakeSeriesNotifier extends ProductSeriesListNotifier {
@override
Future<List<ProductSeriesOption>> build() async => const [];
}
class _FakeSpecNotifier extends ProductSpecListNotifier {
@override
Future<List<ProductSpecOption>> build() async => const [];
}
List<Override> _overrides() => [
inventoryListProvider.overrideWith(() => _FakeInvNotifier()),
productSeriesListProvider.overrideWith(() => _FakeSeriesNotifier()),
productSpecListProvider.overrideWith(() => _FakeSpecNotifier()),
isReadonlyProvider.overrideWithValue(false),
inventorySummaryProvider.overrideWith((ref) => const InventorySummary(
skuCount: 1284,
stockValue: 2640000,
inStockQty: 18920,
shortageCount: 17,
warningCount: 6,
lastMonthSku: 1255,
lastMonthValue: 2611276,
lastMonthQty: 19034,
)),
];
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
goldenAcrossThemes(
'inventory list 窄屏(m-inventory',
goldenPrefix: 'm_inventory',
child: () => const Scaffold(body: InventoryListScreen()),
overrides: _overrides,
logical: const Size(390, 844),
dpr: 2,
);
}