feat(client): 登录/注册页照原型重建,ds 真相源组件族统一全部屏

- 登录/注册(login.html/register.html 1:1):两栏卡片+品牌渐变面板+主题小衣服
  pill(onSurface 变体)+记住我(记录并预填最近账号)+原型式 toast 校验;
  登录 fidelity 1.4–2.1% 三主题全绿;注册暂不入闸(少 门店编号/兑换券 字段,
  已记 CONTRACT,screens.mjs 留存根)
- ds 原子补齐:DsToast(.toast 单例)/DsCheck(.check/.agree)/DsButton lg 档/
  DsSelect 替换全部旧 DropdownButton/DsField label 在上/DsInput 后缀与密码形态
- 全屏统一:对话框按钮全 DsButton、盒式输入主题钉死(visualDensity.standard、
  h38、InputDecorationTheme 渗漏修复)、图标全 lucide、JetBrains Mono 三端同源、
  BrandMark 真相源 logo、只读模式写操作全量守卫(WriteGuard+DsToast)
- 出入库列表:版式对齐原型(卡片对齐+搜索框居中)、KPI 近30天滚动、结清后
  失效财务应收应付表;商品编辑抽屉介绍库改搜索下拉、图片双击全屏预览
- 删除旧 UI 死代码:DataTableCard/FormDialog/PageScaffold/SearchChip/
  SelectProductDialog/tabStateProvider
- golden/fidelity:stock-in/out 补注册(9%)、login 入册(8%)、goldens 全量重打;
  修复 pubspec flutter_web_plugins 非法声明(CI pub get 阻断)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
wangjia
2026-07-03 09:58:14 +08:00
parent ca7595b113
commit 6238b86dcb
286 changed files with 16831 additions and 11136 deletions
+4 -2
View File
@@ -19,7 +19,8 @@ void main() {
);
adapter.onPost(
'/products',
(server) => server.reply(403, {'error': 'readonly user', 'code': 'READONLY_USER'}),
(server) => server
.reply(403, {'error': 'readonly user', 'code': 'READONLY_USER'}),
data: {'name': '测试'},
);
@@ -44,7 +45,8 @@ void main() {
);
adapter.onPost(
'/stock-in',
(server) => server.reply(403, {'error': 'license expired', 'phase': 'readonly'}),
(server) => server
.reply(403, {'error': 'license expired', 'phase': 'readonly'}),
data: {'x': 1},
);
+2 -1
View File
@@ -58,7 +58,8 @@ void main() {
test('401 response maps to AuthException with server message', () async {
adapter.onPost(
'/auth/login',
(server) => server.reply(401, {'error': 'invalid username or password'}),
(server) =>
server.reply(401, {'error': 'invalid username or password'}),
data: {
'shop_code': 'S001',
'username': 'admin',
+2 -1
View File
@@ -1,3 +1,4 @@
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jiu_client/core/theme/themes.dart';
@@ -41,7 +42,7 @@ void main() {
));
await tester.pumpAndSettle();
expect(find.byType(TextField), findsOneWidget);
expect(find.byIcon(Icons.calendar_today), findsOneWidget);
expect(find.byIcon(LucideIcons.calendar), findsOneWidget);
});
testWidgets('初始值回显到输入框', (tester) async {
+87 -15
View File
@@ -1,44 +1,116 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:jiu_client/providers/update_provider.dart';
import 'package:jiu_client/core/utils/clock.dart';
import 'package:jiu_client/providers/changelog_provider.dart';
import 'package:jiu_client/providers/license_provider.dart';
import 'package:jiu_client/screens/about/about_screen.dart';
import '../support/golden_harness.dart';
import '../support/shell_harness.dart';
// 覆盖更新检查器,避免其后台 Timer 在 golden 结束后仍 pending
class _FakeUpdateNotifier extends UpdateNotifier {
/// design-distill 阶段4:关于我们 golden × 三主题(回归闸 + 保真基准)
/// Hero/产品信息/授权信息/更新日志 数据照抄原型 screens/about.html
/// (授权 2027-03-15 · CHANGELOG 4 条,默认 2 条展示):
/// node tools/fidelity.mjs about
/// 更新基准:flutter test --update-goldens test/golden/about_golden_test.dart
class _FakeLicense extends LicenseNotifier {
@override
Future<AppUpdateInfo?> build() async => null;
Future<LicenseInfo?> build() async => LicenseInfo(
id: 1,
type: 'annual',
isActive: true,
maxDevices: 3,
phase: 'normal',
expiresAt: DateTime(2027, 3, 15),
);
}
// 原型 CHANGELOG4 条;v1.0.60/63 属折叠区,默认只展示前 2 条)
const _changelog = [
ChangelogEntry(version: '1.0.72', date: '2026-06-22', sections: [
(
type: 'feat',
items: [
'用户管理新增四级角色:超级管理员 / 管理员 / 操作员 / 只读',
'商品公开页编辑器:图片、品鉴笔记、适饮建议可视化编辑并实时预览',
]
),
(
type: 'impr',
items: [
'顶栏 / 侧栏 / 状态栏统一为单一来源,全局一处生效',
'金额卡片改用「万」单位展示,更符合阅读习惯',
]
),
]),
ChangelogEntry(version: '1.0.66', date: '2026-06-18', sections: [
(
type: 'feat',
items: [
'入库 / 出库列表新增搜索框,支持单号、商品、往来单位关键字检索',
'入库 / 出库 / 库存页新增一键刷新',
]
),
(
type: 'fix',
items: [
'入库审核库存沿用明细真实快照名',
'库存列表过滤软删商品,修复历史导入占位顶替',
]
),
]),
ChangelogEntry(version: '1.0.63', date: '2026-06-05', sections: [
(
type: 'feat',
items: [
'新增设备管理:标签机 / 小票机 / 扫码枪 / 电子秤接入与测试打印',
'商品名搜索支持全拼与首字母',
]
),
]),
ChangelogEntry(version: '1.0.60', date: '2026-05-22', sections: [
(type: 'feat', items: ['往来单位对账单与应收应付明细抽屉']),
]),
];
List<Override> _overrides() => [
updateProvider.overrideWith(() => _FakeUpdateNotifier()),
appVersionProvider.overrideWith((ref) => 'v1.0.72'),
licenseProvider.overrideWith(() => _FakeLicense()),
changelogProvider.overrideWith((ref) async => _changelog),
];
/// design-distill 阶段4:关于我们整屏 golden × 三主题(桌面 + 移动)。
/// 该屏正文已全 token 化;golden 锁住版本/帮助/关于卡片随主题换色。
/// 版本/更新 provider 在测试环境降级为默认值(无插件/无网络)。
/// 更新基准:flutter test --update-goldens test/golden/about_golden_test.dart
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
PackageInfo.setMockInitialValues(
appName: 'jiu',
packageName: 'com.yanmei.jiu',
version: '1.0.72',
buildNumber: '72',
buildSignature: '',
);
// 冻结授权剩余天数基准
appClock = () => DateTime(2026, 7, 13, 9, 30, 15);
goldenAcrossThemes(
'about 整屏(桌面)',
// 桌面:外壳挂载整框
shellGoldenAcrossThemes(
'about 整框(桌面)',
path: '/about',
screen: (_) => const AboutScreen(),
goldenPrefix: 'about',
child: () => const Scaffold(body: AboutScreen()),
overrides: _overrides,
logical: const Size(1280, 1000),
logical: const Size(1280, 900),
);
// 移动:窄屏单列
goldenAcrossThemes(
'about 整屏(移动)',
goldenPrefix: 'about_mobile',
child: () => const Scaffold(body: AboutScreen()),
overrides: _overrides,
logical: const Size(390, 1100),
logical: const Size(390, 1600),
);
}
+14 -4
View File
@@ -25,8 +25,14 @@ class _FakeAuth extends AuthNotifier {
_FakeAuth() {
state = const AuthState(
user: AuthUser(
accessToken: 't', refreshToken: 'r', id: 1, username: 'wang',
realName: '王经理', shopNo: 'DSJH-001', shopId: 1, role: 'admin'),
accessToken: 't',
refreshToken: 'r',
id: 1,
username: 'wang',
realName: '王经理',
shopNo: 'DSJH-001',
shopId: 1,
role: 'admin'),
);
}
}
@@ -42,8 +48,12 @@ class _FakeLicense extends LicenseNotifier {
}
const _shop = ShopInfo(
id: 1, code: 'DSJH-001', name: '鼎晟酒行',
address: '浙江省杭州市', phone: '0571-88886666', managerName: '王经理');
id: 1,
code: 'DSJH-001',
name: '鼎晟酒行',
address: '浙江省杭州市',
phone: '0571-88886666',
managerName: '王经理');
List<Override> _overrides() => [
authStateProvider.overrideWith((ref) => _FakeAuth()),
@@ -2,62 +2,166 @@ 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/models/session.dart';
import 'package:jiu_client/models/shop.dart';
import 'package:jiu_client/providers/session_provider.dart';
import 'package:jiu_client/providers/shop_provider.dart';
import 'package:jiu_client/screens/devices/device_management_screen.dart';
import '../support/golden_harness.dart';
import '../support/shell_harness.dart';
/// design-distill 阶段4:设备管理整屏 golden × 三主题(桌面 + 移动)。
/// 验证在线/离线状态 → StatusPill 随主题换色。
/// design-distill 阶段4:设备管理 golden × 三主题(回归闸 + 保真基准)。
/// 桌面版挂进真 AppShell,数据照抄原型 screens/devices.html 的
/// SESSIONS4 行会话)与 DEVICES5 台外设 → 店级 custom_fields.peripherals):
/// node tools/fidelity.mjs devices
/// 更新基准:flutter test --update-goldens test/golden/device_management_golden_test.dart
List<DeviceSession> _sessions() => [
DeviceSession(
id: 1, userId: 1, username: '王经理', realName: '王经理',
platform: 'macos', platformClass: 'desktop', deviceName: 'MacBook Pro',
ip: '192.168.1.12', createdAt: DateTime(2026, 6, 20, 9, 12),
lastSeenAt: DateTime(2026, 6, 25, 12, 50), online: true, isCurrent: true),
DeviceSession(
id: 2, userId: 2, username: '李销售', realName: '李销售',
platform: 'android', platformClass: 'mobile', deviceName: 'Xiaomi 14',
ip: '192.168.1.23', createdAt: DateTime(2026, 6, 24, 14, 30),
lastSeenAt: DateTime(2026, 6, 25, 11, 05), online: true, isCurrent: false),
DeviceSession(
id: 3, userId: 3, username: '张前台', realName: '张前台',
platform: 'windows', platformClass: 'desktop', deviceName: 'ThinkPad',
ip: '192.168.1.31', createdAt: DateTime(2026, 6, 22, 8, 40),
lastSeenAt: DateTime(2026, 6, 23, 18, 20), online: false, isCurrent: false),
];
// 原型 SESSIONS 4 行(时间 MM-DD HH:mm
final _sessions = [
DeviceSession(
id: 1,
userId: 1,
username: '王经理',
realName: '王经理',
platform: 'macos',
platformClass: 'desktop',
deviceName: 'macos',
ip: '192.168.1.20',
createdAt: DateTime(2026, 6, 20, 11, 23),
lastSeenAt: DateTime(2026, 6, 22, 17, 40),
online: true,
isCurrent: true),
DeviceSession(
id: 2,
userId: 1,
username: '王经理',
realName: '王经理',
platform: 'windows',
platformClass: 'desktop',
deviceName: 'windows',
ip: '192.168.1.21',
createdAt: DateTime(2026, 6, 20, 10, 47),
lastSeenAt: DateTime(2026, 6, 22, 17, 40),
online: true,
isCurrent: false),
DeviceSession(
id: 3,
userId: 2,
username: '李采购',
realName: '李采购',
platform: 'windows',
platformClass: 'desktop',
deviceName: 'windows',
ip: '192.168.1.35',
createdAt: DateTime(2026, 6, 20, 9, 13),
lastSeenAt: DateTime(2026, 6, 22, 17, 11),
online: false,
isCurrent: false),
DeviceSession(
id: 4,
userId: 3,
username: '张前台',
realName: '张前台',
platform: 'ios',
platformClass: 'mobile',
deviceName: 'ios',
ip: '10.0.0.8',
createdAt: DateTime(2026, 6, 21, 17, 9),
lastSeenAt: DateTime(2026, 6, 22, 16, 32),
online: false,
isCurrent: false),
];
// 原型 DEVICES 5 台外设(登记式存档)
const _peripherals = [
{
'name': '前台标签打印机',
'kind': '标签打印机',
'status': '在线',
'model': 'Zebra GK888t',
'conn': 'USB',
'last': '2 分钟前'
},
{
'name': '收银台小票机',
'kind': '小票打印机',
'status': '在线',
'model': '佳博 GP-58MB',
'conn': '蓝牙',
'last': '刚刚'
},
{
'name': '入库扫码枪',
'kind': '扫码枪',
'status': '在线',
'model': 'Honeywell 1900',
'conn': 'USB',
'last': '5 分钟前'
},
{
'name': '仓库电子秤',
'kind': '电子秤',
'status': '离线',
'model': '香山 ACS-30',
'conn': 'WiFi 192.168.1.66',
'last': '昨天 17:42'
},
{
'name': '备用标签机',
'kind': '标签打印机',
'status': '离线',
'model': 'Brother QL-820',
'conn': '网络 192.168.1.51',
'last': '3 天前'
},
];
const _shopWithPeripherals = ShopInfo(
id: 1,
code: 'DSJH-001',
name: '鼎晟酒行',
address: '浙江省杭州市拱墅区运河路 168 号 1 幢',
phone: '0571-8888 6666',
managerName: '王经理',
wechatId: 'dingsheng-wine',
customFields: {'peripherals': _peripherals},
);
class _FakeSessionNotifier extends SessionListNotifier {
@override
Future<List<DeviceSession>> build() async => _sessions();
Future<List<DeviceSession>> build() async => _sessions;
@override
Future<void> reload() async {}
}
List<Override> _overrides() => [
sessionListProvider.overrideWith(() => _FakeSessionNotifier()),
shopInfoProvider.overrideWith((ref) => _shopWithPeripherals),
isReadonlyProvider.overrideWithValue(false),
];
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
goldenAcrossThemes(
'device management 整屏(桌面)',
// 桌面:外壳挂载整框,与原型整页同构
shellGoldenAcrossThemes(
'devices 整框(桌面)',
path: '/devices',
screen: (_) => const DeviceManagementScreen(),
goldenPrefix: 'device_management',
child: () => const Scaffold(body: DeviceManagementScreen()),
overrides: _overrides,
logical: const Size(1280, 900),
);
// 移动:窄屏卡片流
goldenAcrossThemes(
'device management 整屏(移动)',
'devices 整屏(移动)',
goldenPrefix: 'device_management_mobile',
child: () => const Scaffold(body: DeviceManagementScreen()),
overrides: _overrides,
logical: const Size(390, 1100),
logical: const Size(390, 1400),
);
}
+157 -35
View File
@@ -2,73 +2,195 @@ 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/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/repositories/finance_repository.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';
import '../support/shell_harness.dart';
/// design-distill 阶段4:财务整屏 golden × 三主题(桌面 + 移动)。
/// 验证类型徽章(应收/应付/收款/付款)+ 结清状态徽章 → StatusPill 随主题换色。
/// design-distill 阶段4:财务管理 golden × 三主题(回归闸 + 保真基准)。
/// 桌面版挂进真 AppShell,数据照抄原型 screens/finance.html 的
/// TREND6 月)/ SUMMARY8 单位)/ FLOWS10 条);KPI 环比经 stock summary
/// 假数据凑成 ▲8.4% / ▼3.1%
/// node tools/fidelity.mjs finance
/// 更新基准:flutter test --update-goldens test/golden/finance_golden_test.dart
const _records = [
FinanceRecord(
id: 1, type: 'receivable', amount: 126800, balance: 126800,
status: 'open', partnerName: '鼎丰超市', recordDate: '2026-06-20'),
FinanceRecord(
id: 2, type: 'payable', amount: 152600, balance: 0,
status: 'closed', partnerName: '老窖酒水批发', recordDate: '2026-06-18'),
FinanceRecord(
id: 3, type: 'receipt', amount: 54200, balance: 0,
status: 'closed', partnerName: '金樽酒楼', recordDate: '2026-06-15'),
FinanceRecord(
id: 4, type: 'payment', amount: 88400, balance: 88400,
status: 'open', partnerName: '汾酒山西直营', recordDate: '2026-06-12'),
// 原型 TREND(万元 → 元)
const _trend = [
TrendPoint(month: '2026-01', inAmount: 1420000, outAmount: 1180000),
TrendPoint(month: '2026-02', inAmount: 960000, outAmount: 880000),
TrendPoint(month: '2026-03', inAmount: 1680000, outAmount: 1210000),
TrendPoint(month: '2026-04', inAmount: 1540000, outAmount: 1090000),
TrendPoint(month: '2026-05', inAmount: 1720000, outAmount: 1360000),
TrendPoint(month: '2026-06', inAmount: 1860000, outAmount: 1320000),
];
// 财务屏经 FutureBuilder 直读 repository,故覆盖 repoimplements + noSuchMethod
// 只实现 listRecords,其余方法 golden 静态渲染不会触达)。
class _FakeFinanceRepo implements FinanceRepository {
@override
Future<PageResult<FinanceRecord>> listRecords({
String? type,
String? month,
int page = 1,
int pageSize = 50,
}) async =>
const PageResult(data: _records, total: 4, page: 1, pageSize: 20);
// 原型 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 10 条(收款→receipt / 付款→payment;原型 SK/FK 单号无对应
// ref 模型 → 关联单号列显 —,属已知差异)
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: '普五补货'),
FinanceRecord(
id: 6,
type: 'receipt',
amount: 23800,
balance: 0,
status: 'closed',
partnerName: '盛世名酒城',
recordDate: '2026-06-15',
remark: '国窖1573'),
FinanceRecord(
id: 7,
type: 'payment',
amount: 60400,
balance: 0,
status: 'closed',
partnerName: '川渝糖酒集团',
recordDate: '2026-06-14',
remark: '剑南春进货'),
FinanceRecord(
id: 8,
type: 'receipt',
amount: 18900,
balance: 0,
status: 'closed',
partnerName: '御品烟酒行',
recordDate: '2026-06-13',
remark: '习酒窖藏'),
FinanceRecord(
id: 9,
type: 'payment',
amount: 18600,
balance: 0,
status: 'closed',
partnerName: '盛世名酒城',
recordDate: '2026-06-12',
remark: '退换货补差'),
FinanceRecord(
id: 10,
type: 'receipt',
amount: 55200,
balance: 0,
status: 'closed',
partnerName: '华东酒类批发',
recordDate: '2026-06-11',
remark: '梦之蓝M6'),
];
class _FakeFinanceList extends FinanceListNotifier {
@override
dynamic noSuchMethod(Invocation invocation) =>
super.noSuchMethod(invocation);
Future<PageResult<FinanceRecord>> build() async =>
const PageResult(data: _flows, total: 10, 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() => [
financeRepositoryProvider.overrideWithValue(_FakeFinanceRepo()),
financeListProvider.overrideWith(() => _FakeFinanceList()),
financeTrendProvider.overrideWith((ref) async => _trend),
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 整屏(桌面)',
// 桌面:外壳挂载整框,与原型整页同构
shellGoldenAcrossThemes(
'finance 整框(桌面)',
path: '/finance',
screen: (_) => const FinanceScreen(),
goldenPrefix: 'finance',
child: () => const Scaffold(body: FinanceScreen()),
overrides: _overrides,
logical: const Size(1280, 900),
);
// 移动:窄屏单列(KPI 横滚 + 卡片流)
goldenAcrossThemes(
'finance 整屏(移动)',
goldenPrefix: 'finance_mobile',
child: () => const Scaffold(body: FinanceScreen()),
overrides: _overrides,
logical: const Size(390, 1300),
logical: const Size(390, 1600),
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

@@ -11,40 +11,204 @@ import 'package:jiu_client/providers/product_option_provider.dart';
import 'package:jiu_client/screens/inventory/inventory_list_screen.dart';
import '../support/golden_harness.dart';
import '../support/shell_harness.dart';
/// design-distill 阶段4:库存列表**整屏** golden × 三主题(回归闸)。
/// 保真目检:另跑 shoot-prototype 截 screens/inventory.html,再用 montage.mjs
/// 与 goldens/inventory_list_<theme>.png 并排比对。
/// design-distill 阶段4:库存列表 golden × 三主题(回归闸 + 保真基准)。
/// 桌面版挂进真 AppShell(与原型整页截图同构),行数据照抄原型
/// screens/inventory.html 的 ITEMS12 SKU,每页 10),可与 proto 基准整屏 diff:
/// node tools/fidelity.mjs inventory
/// 更新基准:flutter test --update-goldens test/golden/inventory_list_golden_test.dart
// 原型 ITEMS 前 10 行(page 1/12perPage=10)。cost=unitPrice、price=salePrice。
// 状态为派生(qty=0 缺货 / qty<minStock 预警),原型手写状态里
// 剑南春(8)/水晶坊类「非零缺货」在真实数据模型下呈现为预警,属已知语义差。
const _items = [
Inventory(
id: 1, productId: 1, quantity: 128, productCode: 'MT-FT-500',
productName: '茅台 飞天 53°', series: '飞天', spec: '500ml×6', unit: '',
warehouseName: '主仓', unitPrice: 2680, productionDate: '2024-07-11',
batchNo: 'PZ240711', supplierName: '贵州茅台经销', minStock: 10,
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: 8, productCode: 'JNC-SJ-500',
productName: '剑南春 水晶剑', series: '水晶剑', spec: '500ml×6', unit: '',
warehouseName: '主仓', unitPrice: 438, productionDate: '2023-11-15',
batchNo: 'PZ231115', supplierName: '绵竹剑南春', minStock: 10,
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: 3, productId: 3, quantity: 0, productCode: 'FJ-QH20-500',
productName: '汾酒 青花 20', series: '青花20', spec: '500ml×6', unit: '',
warehouseName: '主仓', unitPrice: 420, productionDate: '2024-01-12',
batchNo: 'PZ240112', supplierName: '山西杏花村', minStock: 10,
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: '500ml×6',
unit: '',
warehouseName: '主仓',
unitPrice: 620,
salePrice: 620,
productionDate: '2024-05-20',
batchNo: 'PZ240520',
supplierName: '洋河股份',
minStock: 30,
createdAt: '2026-06-09',
),
Inventory(
id: 6,
productId: 6,
quantity: 156,
productCode: 'LZLJ-TQ-500',
productName: '泸州老窖 特曲',
series: '特曲',
spec: '500ml×6',
unit: '',
warehouseName: '主仓',
unitPrice: 320,
salePrice: 320,
productionDate: '2024-06-30',
batchNo: 'PZ240630',
supplierName: '泸州老窖',
minStock: 10,
createdAt: '2026-06-08',
),
Inventory(
id: 7,
productId: 7,
quantity: 31,
productCode: 'XJ-JC1988',
productName: '习酒 窖藏 1988',
series: '窖藏1988',
spec: '500ml×6',
unit: '',
warehouseName: '主仓',
unitPrice: 760,
salePrice: 760,
productionDate: '2023-09-18',
batchNo: 'PZ230918',
supplierName: '贵州习酒',
minStock: 10,
createdAt: '2026-06-05',
),
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: 9,
productId: 9,
quantity: 54,
productCode: 'LJ-HHL15',
productName: '郎酒 红花郎 15',
series: '红花郎15',
spec: '500ml×6',
unit: '',
warehouseName: '主仓',
unitPrice: 560,
salePrice: 560,
productionDate: '2024-03-25',
batchNo: 'PZ240325',
supplierName: '四川郎酒',
minStock: 10,
createdAt: '2026-06-01',
),
Inventory(
id: 10,
productId: 10,
quantity: 12,
productCode: 'GJG-NF-500',
productName: '古井贡 年份原浆',
series: '年份原浆',
spec: '500ml×6',
unit: '',
warehouseName: '主仓',
unitPrice: 390,
salePrice: 390,
productionDate: '2024-08-02',
batchNo: 'PZ240802',
supplierName: '古井贡酒',
minStock: 20,
createdAt: '2026-05-28',
),
];
class _FakeInvNotifier extends InventoryListNotifier {
@override
Future<PageResult<Inventory>> build() async =>
const PageResult(data: _items, total: 3, page: 1, pageSize: 20);
const PageResult(data: _items, total: 12, page: 1, pageSize: 10);
@override
void setPage(int page) {}
@override
@@ -54,6 +218,8 @@ class _FakeInvNotifier extends InventoryListNotifier {
@override
void setKeyword(String keyword) {}
@override
void setCode(String code) {}
@override
void setSeries(List<String> series) {}
@override
void setSpec(List<String> spec) {}
@@ -82,6 +248,10 @@ List<Override> _overrides() => [
inStockQty: 18920,
shortageCount: 17,
warningCount: 6,
// 月初快照 → 环比 ▲2.3% / ▲1.1% / ▼0.6%(对齐原型示例文案)
lastMonthSku: 1255,
lastMonthValue: 2611276,
lastMonthQty: 19034,
)),
];
@@ -89,11 +259,12 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
// 桌面:宽屏表格路径
goldenAcrossThemes(
'inventory list 整(桌面)',
// 桌面:外壳挂载整框(顶栏+侧栏+状态栏+屏),与原型整页同构
shellGoldenAcrossThemes(
'inventory list 整(桌面)',
path: '/inventory',
screen: (_) => const InventoryListScreen(),
goldenPrefix: 'inventory_list',
child: () => const Scaffold(body: InventoryListScreen()),
overrides: _overrides,
logical: const Size(1280, 900),
);
+30 -14
View File
@@ -1,27 +1,43 @@
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/theme/theme_controller.dart';
import 'package:jiu_client/providers/connectivity_provider.dart';
import 'package:jiu_client/screens/auth/login_screen.dart';
import '../support/golden_harness.dart';
/// design-distill 阶段4:登录屏 golden × 三主题(品牌 hero + 表单)。
/// 锁住品牌 hero、表单、离线提示条(已 token 化)随主题换色
/// design-distill 阶段4:登录屏 golden × 三主题(照原型 login.html 重建:
/// 两栏卡片=品牌渐变面板+表单,右上角主题切换器)。1280×900 与 fidelity 注册一致
/// 历史账号 mock 成原型预填值(DSJH-001 / wang),记住我默认开 → 表单预填。
/// 更新基准:flutter test --update-goldens test/golden/login_golden_test.dart
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
SharedPreferences.setMockInitialValues({
'login_history_hotels': ['DSJH-001'],
'login_history_usernames': ['wang'],
});
goldenAcrossThemes(
'login 整屏',
goldenPrefix: 'login',
child: () => const LoginScreen(),
overrides: () => [
// 跳过联网首检,避免后台 Timer 在 golden 结束后仍 pending
connectivityProvider.overrideWith((ref) => ConnectivityNotifier(skipInit: true)),
],
logical: const Size(1280, 860),
);
for (final theme in const ['a', 'b', 'c']) {
testWidgets('login 整屏 · theme $theme', (tester) async {
await pumpGolden(
tester,
child: const LoginScreen(),
theme: theme,
goldenName: 'login_$theme',
overrides: [
// 跳过联网首检,避免后台 Timer 在 golden 结束后仍 pending
connectivityProvider
.overrideWith((ref) => ConnectivityNotifier(skipInit: true)),
// 主题切换器选中态跟随当前主题(与原型 setTheme 同步高亮一致)
themeControllerProvider.overrideWith((ref) {
final c = ThemeController();
c.set(theme);
return c;
}),
],
logical: const Size(1280, 900),
);
});
}
}
+99 -43
View File
@@ -9,41 +9,106 @@ import 'package:jiu_client/providers/partner_provider.dart';
import 'package:jiu_client/screens/partners/partners_screen.dart';
import '../support/golden_harness.dart';
import '../support/shell_harness.dart';
/// design-distill 阶段4:往来单位整屏 golden × 三主题(桌面 + 移动,回归闸)。
/// 保真目检:shoot partners.html + montage 与 goldens/partners_*.png 并排。
/// design-distill 阶段4:往来单位 golden × 三主题(回归闸 + 保真基准)。
/// 桌面版挂进真 AppShell(与原型整页截图同构),行数据照抄原型
/// screens/partners.html 的 PARTNERS10 行),可与 proto 基准整屏 diff
/// node tools/fidelity.mjs partners
/// 更新基准:flutter test --update-goldens test/golden/partners_golden_test.dart
const _suppliers = [
// 原型 PARTNERS 10 行(recv/pay 走 partnerBalancesProvider 假数据)。
const _partners = [
Partner(
id: 1, code: 'GYS-001', name: '茅台华东总代', type: 'supplier',
contact: '张伟', phone: '138-0011-2233', address: '上海市黄浦区中山东路 8 号',
status: 'enabled'),
id: 1,
name: '茅台华东总代',
type: 'supplier',
contact: '张伟',
phone: '138-0011-2233',
address: '上海市青浦区华新镇酒类商城A12'),
Partner(
id: 2, code: 'GYS-002', name: '老窖酒水批发', type: 'supplier',
contact: '李建国', phone: '139-2200-7788', address: '江苏省南京市建邺区江东中路',
status: 'enabled'),
id: 2,
name: '老李酒水批发',
type: 'supplier,customer',
contact: '李建国',
phone: '139-2200-7788',
address: '江苏省南京市江宁区秣陵街道'),
Partner(
id: 3, code: 'GYS-003', name: '五粮液川南运营中心', type: 'supplier',
contact: '刘海涛', phone: '135-8899-0011', address: '四川省宜宾市翠屏区岷江路',
status: 'disabled'),
id: 3,
name: '鼎丰超市',
type: 'customer',
contact: '王芳',
phone: '137-6655-4321',
address: '浙江省杭州市拱墅区大关路88号'),
Partner(
id: 4,
name: '五粮液川南运营中心',
type: 'supplier',
contact: '刘德海',
phone: '135-8899-0011',
address: '四川省宜宾市翠屏区岷江路'),
Partner(
id: 5,
name: '金樽酒楼',
type: 'customer',
contact: '陈静',
phone: '136-2211-9900',
address: '江苏省苏州市姑苏区观前街'),
Partner(
id: 6,
name: '恒昌名酒行',
type: 'supplier,customer',
contact: '赵明',
phone: '138-7766-5544',
address: '安徽省合肥市蜀山区长江西路'),
Partner(
id: 7,
name: '汾酒山西直营',
type: 'supplier',
contact: '孙立',
phone: '134-5566-7788',
address: '山西省吕梁市汾阳市杏花村'),
Partner(
id: 8,
name: '喜宴婚庆服务',
type: 'customer',
contact: '周丽',
phone: '139-3344-5566',
address: '江苏省无锡市梁溪区中山路'),
Partner(
id: 9,
name: '郎酒华东仓储',
type: 'supplier',
contact: '吴强',
phone: '137-9900-1122',
address: '江苏省南京市六合区雄州街道'),
Partner(
id: 10,
name: '万家乐连锁便利',
type: 'customer',
contact: '郑涛',
phone: '135-1122-3344',
address: '浙江省宁波市海曙区中山东路'),
];
const _customers = [
Partner(
id: 11, code: 'KH-001', name: '鼎丰超市', type: 'customer',
contact: '王芳', phone: '137-6655-4321', address: '浙江省杭州市西湖区文三路',
status: 'enabled'),
Partner(
id: 12, code: 'KH-002', name: '金樽酒楼', type: 'customer',
contact: '陈静', phone: '136-2211-9900', address: '江苏省苏州市姑苏区干将路',
status: 'enabled'),
];
// 原型 recv/pay 数值(id → (应收, 应付))
const _balances = <int, ({double recv, double pay})>{
1: (recv: 0, pay: 486200),
2: (recv: 38400, pay: 152600),
3: (recv: 126800, pay: 0),
4: (recv: 0, pay: 298500),
5: (recv: 54200, pay: 0),
6: (recv: 72900, pay: 41000),
7: (recv: 0, pay: 88400),
8: (recv: 19600, pay: 0),
9: (recv: 0, pay: 134700),
10: (recv: 93500, pay: 0),
};
class _FakeSupplierNotifier extends PartnerListNotifier {
class _FakePartnersNotifier extends PartnerListNotifier {
@override
Future<PageResult<Partner>> build() async => const PageResult(
data: _suppliers, total: 3, page: 1, pageSize: 20);
Future<PageResult<Partner>> build() async =>
const PageResult(data: _partners, total: 10, page: 1, pageSize: 1000);
@override
void setPage(int page) {}
@override
@@ -51,26 +116,14 @@ class _FakeSupplierNotifier extends PartnerListNotifier {
@override
void setKeyword(String keyword) {}
@override
void reload() {}
}
class _FakeCustomerNotifier extends PartnerListNotifier {
@override
Future<PageResult<Partner>> build() async => const PageResult(
data: _customers, total: 2, page: 1, pageSize: 20);
@override
void setPage(int page) {}
@override
void setPageSize(int pageSize) {}
@override
void setKeyword(String keyword) {}
void setType(String? t) {}
@override
void reload() {}
}
List<Override> _overrides() => [
supplierListProvider.overrideWith(() => _FakeSupplierNotifier()),
customerListProvider.overrideWith(() => _FakeCustomerNotifier()),
partnersScreenProvider.overrideWith(() => _FakePartnersNotifier()),
partnerBalancesProvider.overrideWith((ref) async => _balances),
isReadonlyProvider.overrideWithValue(false),
];
@@ -78,14 +131,17 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
goldenAcrossThemes(
'partners 整屏(桌面)',
// 桌面:外壳挂载整框(顶栏+侧栏+状态栏+屏),与原型整页同构
shellGoldenAcrossThemes(
'partners 整框(桌面)',
path: '/partners',
screen: (_) => const PartnersScreen(),
goldenPrefix: 'partners',
child: () => const Scaffold(body: PartnersScreen()),
overrides: _overrides,
logical: const Size(1280, 900),
);
// 移动:窄屏(<600)→ context.isMobile → MobileListCard 卡片流路径
goldenAcrossThemes(
'partners 整屏(移动)',
goldenPrefix: 'partners_mobile',
@@ -21,9 +21,15 @@ import '../support/golden_harness.dart';
class _FakeProductRepo implements ProductRepository {
@override
Future<Product> getDetail(int id) async => const Product(
id: 1, code: 'MT-FT-500', name: '茅台 飞天 53°', unit: '',
series: '飞天', spec: '500ml×6', brand: '茅台',
purchasePrice: 2680, categoryName: '酱香');
id: 1,
code: 'MT-FT-500',
name: '茅台 飞天 53°',
unit: '',
series: '飞天',
spec: '500ml×6',
brand: '茅台',
purchasePrice: 2680,
categoryName: '酱香');
@override
Future<List<({String date, double price})>> getPriceHistory(int id) async =>
const [
@@ -33,8 +39,7 @@ class _FakeProductRepo implements ProductRepository {
(date: '2025-11-15', price: 2580),
];
@override
dynamic noSuchMethod(Invocation invocation) =>
super.noSuchMethod(invocation);
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeInvRepo implements InventoryRepository {
@@ -43,6 +48,7 @@ class _FakeInvRepo implements InventoryRepository {
int? warehouseId,
int? productId,
String? keyword,
String? code,
List<String>? series,
List<String>? spec,
int page = 1,
@@ -63,22 +69,33 @@ class _FakeInvRepo implements InventoryRepository {
}) async =>
const PageResult(data: [
InventoryLog(
warehouseId: 1, productId: 1, direction: 'in', quantity: 24,
warehouseId: 1,
productId: 1,
direction: 'in',
quantity: 24,
createdAt: '2026-06-20T09:00:00'),
InventoryLog(
warehouseId: 1, productId: 1, direction: 'out', quantity: 12,
warehouseId: 1,
productId: 1,
direction: 'out',
quantity: 12,
createdAt: '2026-06-18T09:00:00'),
InventoryLog(
warehouseId: 2, productId: 1, direction: 'out', quantity: 18,
warehouseId: 2,
productId: 1,
direction: 'out',
quantity: 18,
createdAt: '2026-06-15T09:00:00'),
InventoryLog(
warehouseId: 1, productId: 1, direction: 'in', quantity: 36,
warehouseId: 1,
productId: 1,
direction: 'in',
quantity: 36,
createdAt: '2026-06-12T09:00:00'),
], total: 4, page: 1, pageSize: 100);
@override
dynamic noSuchMethod(Invocation invocation) =>
super.noSuchMethod(invocation);
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
List<Override> _overrides() => [
@@ -9,27 +9,117 @@ import 'package:jiu_client/providers/product_provider.dart';
import 'package:jiu_client/screens/products/products_screen.dart';
import '../support/golden_harness.dart';
import '../support/shell_harness.dart';
/// design-distill 阶段4:基础数据「商品档案」全量商品表 golden × 三主题。
/// 锁住原型头牌 tab(商品 name+code / 系列 / 规格 / 分类 / 单位 / 默认成本价)。
/// design-distill 阶段4:基础数据「商品档案」golden × 三主题(回归闸 + 保真基准)
/// 桌面版挂进真 AppShell,行数据照抄原型 screens/products.html 的 PRODUCTS12 行,
/// 去掉分类/状态字段——香型弃用、无 status 后端,见 CONTRACT 已知差异):
/// node tools/fidelity.mjs products
/// 更新基准:flutter test --update-goldens test/golden/products_archive_golden_test.dart
const _products = [
Product(
id: 1, code: 'MT-FT-500', name: '茅台 飞天 53°', unit: '',
series: '飞天', spec: '500ml×6', categoryName: '酱香', purchasePrice: 2680),
id: 1,
code: 'MT-FT-500',
name: '茅台 飞天',
unit: '',
series: '飞天',
spec: '500ml×6 · 53°',
purchasePrice: 2680),
Product(
id: 2, code: 'WLY-PW-500', name: '五粮液 普五 52°', unit: '',
series: '普五', spec: '500ml×6', categoryName: '浓香', purchasePrice: 1050),
id: 2,
code: 'MT-FT-100',
name: '茅台 飞天',
unit: '',
series: '飞天',
spec: '100ml×24 · 53°',
purchasePrice: 620),
Product(
id: 3, code: 'JNC-SJ-500', name: '剑南春 水晶剑', unit: '',
series: '水晶剑', spec: '500ml×6', categoryName: '浓香', purchasePrice: 438),
id: 3,
code: 'WLY-PW-500',
name: '五粮液 普五',
unit: '',
series: '第八代',
spec: '500ml×6 · 52°',
purchasePrice: 1050),
Product(
id: 4,
code: 'GJ-1573-500',
name: '国窖 1573',
unit: '',
series: '经典装',
spec: '500ml×6 · 52°',
purchasePrice: 980),
Product(
id: 5,
code: 'JNC-SJ-500',
name: '剑南春 水晶剑',
unit: '',
series: '水晶剑',
spec: '500ml×6 · 52°',
purchasePrice: 438),
Product(
id: 6,
code: 'YH-M6-500',
name: '洋河 梦之蓝',
unit: '',
series: 'M6+',
spec: '550ml×6 · 45°',
purchasePrice: 620),
Product(
id: 7,
code: 'LZLJ-TQ-500',
name: '泸州老窖 特曲',
unit: '',
series: '第十代',
spec: '500ml×6 · 52°',
purchasePrice: 320),
Product(
id: 8,
code: 'XJ-JC1988',
name: '习酒 窖藏',
unit: '',
series: '1988',
spec: '500ml×6 · 53°',
purchasePrice: 760),
Product(
id: 9,
code: 'FJ-QH20-500',
name: '汾酒 青花',
unit: '',
series: '青花20',
spec: '500ml×6 · 53°',
purchasePrice: 420),
Product(
id: 10,
code: 'LJ-HHL15',
name: '郎酒 红花郎',
unit: '',
series: '红花郎15',
spec: '500ml×6 · 53°',
purchasePrice: 560),
Product(
id: 11,
code: 'XF-QJ-500',
name: '西凤酒 旗舰版',
unit: '',
series: '旗舰',
spec: '500ml×6 · 52°',
purchasePrice: 280),
Product(
id: 12,
code: 'GJG-NF-500',
name: '古井贡 年份原浆',
unit: '',
series: '古16',
spec: '500ml×6 · 50°',
purchasePrice: 390),
];
class _FakeProductList extends ProductListNotifier {
@override
Future<PageResult<Product>> build() async =>
const PageResult(data: _products, total: 3, page: 1, pageSize: 20);
const PageResult(data: _products, total: 12, page: 1, pageSize: 20);
@override
void setPage(int page) {}
@override
@@ -42,18 +132,31 @@ class _FakeProductList extends ProductListNotifier {
void reload() {}
}
List<Override> _overrides() => [
productListProvider.overrideWith(() => _FakeProductList()),
isReadonlyProvider.overrideWithValue(false),
];
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
goldenAcrossThemes(
'products 商品档案 tab',
// 桌面:外壳挂载整框,与原型整页同构
shellGoldenAcrossThemes(
'products 商品档案 整框(桌面)',
path: '/products',
screen: (_) => const ProductsScreen(),
goldenPrefix: 'products_archive',
overrides: _overrides,
logical: const Size(1280, 900),
);
// 移动:窄屏卡片流
goldenAcrossThemes(
'products 商品档案 整屏(移动)',
goldenPrefix: 'products_archive_mobile',
child: () => const Scaffold(body: ProductsScreen()),
overrides: () => [
productListProvider.overrideWith(() => _FakeProductList()),
isReadonlyProvider.overrideWithValue(false),
],
logical: const Size(1280, 760),
overrides: _overrides,
logical: const Size(390, 1400),
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 KiB

After

Width:  |  Height:  |  Size: 434 KiB

Some files were not shown because too many files have changed in this diff Show More