diff --git a/client/lib/screens/partners/partners_screen.dart b/client/lib/screens/partners/partners_screen.dart index 21d9df5..3b7bac2 100644 --- a/client/lib/screens/partners/partners_screen.dart +++ b/client/lib/screens/partners/partners_screen.dart @@ -6,9 +6,11 @@ import '../../core/models/page_result.dart'; import '../../core/responsive/responsive.dart'; import '../../core/config/app_constants.dart'; import '../../core/theme/context_tokens.dart'; +import '../../core/theme/app_dims.g.dart'; import '../../models/partner.dart'; import '../../providers/partner_provider.dart'; import '../../widgets/data_table_card.dart'; +import '../../widgets/kpi_card.dart'; import '../../widgets/mobile_list_card.dart'; import '../../widgets/page_scaffold.dart'; import '../../core/utils/export_util.dart'; @@ -154,15 +156,7 @@ class _PartnersScreenState extends ConsumerState { return MobileListCard( title: Text(p.name), subtitle: p.code != null ? Text(p.code!) : null, - trailing: Text( - p.status == 'enabled' ? '正常' : '禁用', - style: TextStyle( - fontSize: 12, - color: p.status == 'enabled' - ? context.tokens.success - : context.tokens.muted, - ), - ), + trailing: _PartnerStatusPill(p.status), fields: [ if (p.contact?.isNotEmpty == true) MobileCardField('联系人', p.contact), if (p.phone?.isNotEmpty == true) MobileCardField('电话', p.phone), @@ -190,24 +184,48 @@ class _PartnersScreenState extends ConsumerState { ); } - return DataTableCard( + return Column( + children: [ + // 副标(还原原型 .head .sub) + Container( + width: double.infinity, + color: context.tokens.bg, + padding: const EdgeInsets.fromLTRB( + AppDims.sp3, AppDims.sp3, AppDims.sp3, 0), + child: Text( + '共 ${result.total} 个${isSupplier ? '供应商' : '客户'}', + style: TextStyle( + fontSize: AppDims.fsSm, color: context.tokens.muted), + ), + ), + Expanded( + child: DataTableCard( totalCount: result.total, page: result.page, pageSize: result.pageSize, onPageChanged: onPageChanged, onPageSizeChanged: onPageSizeChanged, mobileCards: partners.map(partnerCard).toList(), - toolbar: Row( - children: [ + toolbar: Builder(builder: (ctx) { + final searchField = TextField( + controller: searchCtrl, + decoration: InputDecoration( + hintText: isSupplier ? '搜索供应商名称/编码' : '搜索客户名称/编码', + prefixIcon: const Icon(Icons.search, size: 16), + hintStyle: const TextStyle(fontSize: 13), + ), + onChanged: onSearchChanged, + ); + final buttons = [ if (!WriteGuard.isReadonly(ref)) ...[ WriteGuard( child: ElevatedButton.icon( onPressed: onAdd, icon: const Icon(Icons.add, size: 16), - label: Text(isSupplier ? '新建' : '新建'), + label: const Text('新建'), ), ), - const SizedBox(width: 8), + const SizedBox(width: AppDims.sp2), ], OutlinedButton.icon( onPressed: () => exportExcel( @@ -221,34 +239,37 @@ class _PartnersScreenState extends ConsumerState { icon: const Icon(Icons.download, size: 16), label: const Text('导出'), ), - const Spacer(), - SizedBox( - width: 200, - child: TextField( - controller: searchCtrl, - decoration: InputDecoration( - hintText: isSupplier ? '搜索供应商名称/编码' : '搜索客户名称/编码', - prefixIcon: const Icon(Icons.search, size: 16), - hintStyle: const TextStyle(fontSize: 13), - ), - onChanged: onSearchChanged, - ), - ), - ], - ), - columns: [ - const DataColumn(label: Text('编码')), - const DataColumn(label: Text('名称')), - const DataColumn(label: Text('联系人')), - const DataColumn(label: Text('联系电话')), - const DataColumn(label: Text('地址')), - const DataColumn(label: Text('状态')), - const DataColumn(label: Text('操作')), + ]; + if (ctx.isMobile) { + // 窄屏:搜索独占一行 + 操作按钮一行 + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + searchField, + const SizedBox(height: AppDims.sp2), + Row(children: [...buttons, const Spacer()]), + ], + ); + } + return Row( + children: [ + ...buttons, + const Spacer(), + SizedBox(width: 240, child: searchField), + ], + ); + }), + columns: const [ + DataColumn(label: Text('名称')), + DataColumn(label: Text('联系人')), + DataColumn(label: Text('联系电话')), + DataColumn(label: Text('地址')), + DataColumn(label: Text('状态')), + DataColumn(label: Text('操作')), ], rows: partners.isEmpty ? [ DataRow(cells: [ - const DataCell(SizedBox()), DataCell(Text(isSupplier ? '暂无供应商' : '暂无客户', style: TextStyle(color: context.tokens.muted))), @@ -262,17 +283,24 @@ class _PartnersScreenState extends ConsumerState { : partners .map((p) => DataRow( cells: [ - DataCell(Text(p.code ?? '-', - style: TextStyle( - fontFamily: 'monospace', - fontSize: 12, - color: context.tokens.muted))), DataCell(SizedBox( - width: 180, - child: Text(p.name, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.w500)), + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(p.name, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w600)), + if ((p.code ?? '').isNotEmpty) + Text(p.code!, + style: TextStyle( + fontFamily: 'monospace', + fontSize: AppDims.fsXs, + color: context.tokens.muted)), + ], + ), )), DataCell(Text(p.contact ?? '-')), DataCell(Text(p.phone ?? '-')), @@ -281,15 +309,7 @@ class _PartnersScreenState extends ConsumerState { child: Text(p.address ?? '-', overflow: TextOverflow.ellipsis), )), - DataCell(Text( - p.status == 'enabled' ? '正常' : '禁用', - style: TextStyle( - fontSize: 13, - color: p.status == 'enabled' - ? context.tokens.success - : context.tokens.muted, - ), - )), + DataCell(_PartnerStatusPill(p.status)), DataCell(Row( mainAxisSize: MainAxisSize.min, children: WriteGuard.isReadonly(ref) @@ -318,6 +338,9 @@ class _PartnersScreenState extends ConsumerState { ], )) .toList(), + ), + ), + ], ); } @@ -388,6 +411,20 @@ class _PartnersScreenState extends ConsumerState { } } +// 往来单位状态徽章(启用/停用),还原原型 .badge 圆点软底。 +class _PartnerStatusPill extends StatelessWidget { + final String status; + const _PartnerStatusPill(this.status); + + @override + Widget build(BuildContext context) { + final t = context.tokens; + return status == 'enabled' + ? StatusPill(label: '启用', color: t.success, background: t.okSoft) + : StatusPill(label: '停用', color: t.muted, background: t.borderSubtle); + } +} + class _PartnerFormDialog extends ConsumerStatefulWidget { final bool isSupplier; final Partner? partner; diff --git a/client/test/golden/goldens/partners_a.png b/client/test/golden/goldens/partners_a.png new file mode 100644 index 0000000..0546362 Binary files /dev/null and b/client/test/golden/goldens/partners_a.png differ diff --git a/client/test/golden/goldens/partners_b.png b/client/test/golden/goldens/partners_b.png new file mode 100644 index 0000000..35b2db5 Binary files /dev/null and b/client/test/golden/goldens/partners_b.png differ diff --git a/client/test/golden/goldens/partners_c.png b/client/test/golden/goldens/partners_c.png new file mode 100644 index 0000000..1178c9f Binary files /dev/null and b/client/test/golden/goldens/partners_c.png differ diff --git a/client/test/golden/goldens/partners_mobile_a.png b/client/test/golden/goldens/partners_mobile_a.png new file mode 100644 index 0000000..c87461f Binary files /dev/null and b/client/test/golden/goldens/partners_mobile_a.png differ diff --git a/client/test/golden/goldens/partners_mobile_b.png b/client/test/golden/goldens/partners_mobile_b.png new file mode 100644 index 0000000..c91b50a Binary files /dev/null and b/client/test/golden/goldens/partners_mobile_b.png differ diff --git a/client/test/golden/goldens/partners_mobile_c.png b/client/test/golden/goldens/partners_mobile_c.png new file mode 100644 index 0000000..00aa6e7 Binary files /dev/null and b/client/test/golden/goldens/partners_mobile_c.png differ diff --git a/client/test/golden/partners_golden_test.dart b/client/test/golden/partners_golden_test.dart new file mode 100644 index 0000000..d563480 --- /dev/null +++ b/client/test/golden/partners_golden_test.dart @@ -0,0 +1,96 @@ +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/partner.dart'; +import 'package:jiu_client/providers/partner_provider.dart'; +import 'package:jiu_client/screens/partners/partners_screen.dart'; + +import '../support/golden_harness.dart'; + +/// design-distill 阶段4:往来单位整屏 golden × 三主题(桌面 + 移动,回归闸)。 +/// 保真目检:shoot partners.html + montage 与 goldens/partners_*.png 并排。 +/// 更新基准:flutter test --update-goldens test/golden/partners_golden_test.dart + +const _suppliers = [ + Partner( + id: 1, code: 'GYS-001', name: '茅台华东总代', type: 'supplier', + contact: '张伟', phone: '138-0011-2233', address: '上海市黄浦区中山东路 8 号', + status: 'enabled'), + Partner( + id: 2, code: 'GYS-002', name: '老窖酒水批发', type: 'supplier', + contact: '李建国', phone: '139-2200-7788', address: '江苏省南京市建邺区江东中路', + status: 'enabled'), + Partner( + id: 3, code: 'GYS-003', name: '五粮液川南运营中心', type: 'supplier', + contact: '刘海涛', phone: '135-8899-0011', address: '四川省宜宾市翠屏区岷江路', + status: 'disabled'), +]; + +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'), +]; + +class _FakeSupplierNotifier extends PartnerListNotifier { + @override + Future> build() async => const PageResult( + data: _suppliers, total: 3, page: 1, pageSize: 20); + @override + void setPage(int page) {} + @override + void setPageSize(int pageSize) {} + @override + void setKeyword(String keyword) {} + @override + void reload() {} +} + +class _FakeCustomerNotifier extends PartnerListNotifier { + @override + Future> 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) {} + @override + void reload() {} +} + +List _overrides() => [ + supplierListProvider.overrideWith(() => _FakeSupplierNotifier()), + customerListProvider.overrideWith(() => _FakeCustomerNotifier()), + isReadonlyProvider.overrideWithValue(false), + ]; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + + goldenAcrossThemes( + 'partners 整屏(桌面)', + goldenPrefix: 'partners', + child: () => const Scaffold(body: PartnersScreen()), + overrides: _overrides, + logical: const Size(1280, 900), + ); + + goldenAcrossThemes( + 'partners 整屏(移动)', + goldenPrefix: 'partners_mobile', + child: () => const Scaffold(body: PartnersScreen()), + overrides: _overrides, + logical: const Size(390, 1200), + ); +}