diff --git a/client/lib/screens/products/products_screen.dart b/client/lib/screens/products/products_screen.dart index f19280e..dc0bc0b 100644 --- a/client/lib/screens/products/products_screen.dart +++ b/client/lib/screens/products/products_screen.dart @@ -1,10 +1,14 @@ import '../../core/utils/dialog_util.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import '../../core/responsive/responsive.dart'; import '../../core/theme/context_tokens.dart'; +import '../../core/theme/app_dims.g.dart'; import '../../models/warehouse.dart'; +import '../../models/product.dart'; import '../../providers/product_option_provider.dart'; +import '../../providers/product_provider.dart'; import '../../models/product_option.dart' show ProductCategoryOption; import '../../providers/warehouse_provider.dart'; import '../../providers/shop_provider.dart'; @@ -25,6 +29,7 @@ class _ProductsScreenState extends ConsumerState { // 「设为默认」的乐观覆盖:点一下立刻变色,不等后端回包(key→选项 id)。 final Map _optDefaults = {}; + final _archiveSearchCtrl = TextEditingController(); final _nameSearchCtrl = TextEditingController(); final _seriesSearchCtrl = TextEditingController(); final _specSearchCtrl = TextEditingController(); @@ -61,6 +66,7 @@ class _ProductsScreenState extends ConsumerState { _storageSearchCtrl.dispose(); _descDocSearchCtrl.dispose(); _categorySearchCtrl.dispose(); + _archiveSearchCtrl.dispose(); super.dispose(); } @@ -68,31 +74,167 @@ class _ProductsScreenState extends ConsumerState { Widget build(BuildContext context) { return PageScaffold( title: '基础数据', + // 原型 5 个 tab(商品档案/商品介绍/系列/规格/香型)置前; + // 其余字典(商品名称/产地/保质期/储存/仓库)保留在后,不删管理入口。 tabs: const [ - Tab(text: '商品名称'), + Tab(text: '商品档案'), + Tab(text: '商品介绍'), Tab(text: '系列'), Tab(text: '规格'), Tab(text: '香型'), + Tab(text: '商品名称'), Tab(text: '产地'), Tab(text: '保质期'), Tab(text: '储存方式'), - Tab(text: '描述文档'), Tab(text: '仓库'), ], tabViews: [ - _buildNameTab(), + _buildArchiveTab(), + _buildDescDocTab(), _buildSeriesTab(), _buildSpecTab(), _buildCategoryTab(), + _buildNameTab(), _buildOriginTab(), _buildShelfLifeTab(), _buildStorageTab(), - _buildDescDocTab(), _buildWarehousesTab(), ], ); } + // ── 商品档案 tab(全量商品表,还原原型头牌) ──────────────────── + Widget _buildArchiveTab() { + final async = ref.watch(productListProvider); + return async.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => + _buildError(() => ref.read(productListProvider.notifier).reload()), + data: (result) { + final items = result.data; + final notifier = ref.read(productListProvider.notifier); + return DataTableCard( + totalCount: result.total, + page: result.page, + pageSize: result.pageSize, + onPageChanged: (p) => notifier.setPage(p), + onPageSizeChanged: (s) => notifier.setPageSize(s), + toolbar: Row( + children: [ + SizedBox( + width: 240, + child: TextField( + controller: _archiveSearchCtrl, + decoration: const InputDecoration( + hintText: '商品名 / 编码 / 拼音,回车搜索', + prefixIcon: Icon(Icons.search, size: 16), + hintStyle: TextStyle(fontSize: 13), + ), + onSubmitted: (v) => notifier.setKeyword(v.trim()), + ), + ), + const SizedBox(width: AppDims.sp2), + OutlinedButton.icon( + onPressed: () => exportExcel( + filename: '商品档案', + headers: ['商品编码', '商品名称', '系列', '规格', '分类', '单位', '默认成本价'], + rows: items + .map((p) => [ + p.code, + p.name, + p.series ?? '', + p.spec ?? '', + p.categoryName ?? '', + p.unit, + p.purchasePrice ?? '', + ]) + .toList(), + ), + icon: const Icon(Icons.download, size: 16), + label: const Text('导出'), + ), + const Spacer(), + ], + ), + mobileCards: items + .map((p) => MobileListCard( + onTap: () => context.push('/products/${p.id}'), + title: Text(p.name), + subtitle: p.code.isEmpty ? null : Text(p.code), + fields: [ + if ((p.series ?? '').isNotEmpty) + MobileCardField('系列', p.series), + if ((p.spec ?? '').isNotEmpty) + MobileCardField('规格', p.spec), + if ((p.categoryName ?? '').isNotEmpty) + MobileCardField('分类', p.categoryName), + MobileCardField('单位', p.unit.isEmpty ? '-' : p.unit), + ], + )) + .toList(), + columns: const [ + DataColumn(label: Text('商品')), + DataColumn(label: Text('系列')), + DataColumn(label: Text('规格')), + DataColumn(label: Text('分类')), + DataColumn(label: Text('单位')), + DataColumn(label: Text('默认成本价')), + DataColumn(label: Text('操作')), + ], + rows: items.isEmpty + ? [ + DataRow(cells: [ + DataCell(Text('暂无商品(入库录单后自动建档)', + style: TextStyle(color: context.tokens.muted))), + for (int i = 1; i < 7; i++) const DataCell(SizedBox()), + ]) + ] + : items + .map((p) => DataRow( + onSelectChanged: (_) => context.push('/products/${p.id}'), + cells: [ + DataCell(SizedBox( + 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.series ?? '').isEmpty ? '-' : p.series!)), + DataCell(Text((p.spec ?? '').isEmpty ? '-' : p.spec!)), + DataCell(Text((p.categoryName ?? '').isEmpty + ? '-' + : p.categoryName!)), + DataCell(Text(p.unit.isEmpty ? '-' : p.unit)), + DataCell(Text( + p.purchasePrice != null && p.purchasePrice! > 0 + ? '¥${p.purchasePrice!.toStringAsFixed(2)}' + : '-')), + DataCell(TextButton( + onPressed: () => context.push('/products/${p.id}'), + child: const Text('查看', + style: TextStyle(fontSize: 12)), + )), + ], + )) + .toList(), + ); + }, + ); + } + // ── 商品名称 tab ─────────────────────────────────────────── Widget _buildNameTab() { diff --git a/client/test/golden/goldens/products_archive_a.png b/client/test/golden/goldens/products_archive_a.png new file mode 100644 index 0000000..fc0027a Binary files /dev/null and b/client/test/golden/goldens/products_archive_a.png differ diff --git a/client/test/golden/goldens/products_archive_b.png b/client/test/golden/goldens/products_archive_b.png new file mode 100644 index 0000000..95a935b Binary files /dev/null and b/client/test/golden/goldens/products_archive_b.png differ diff --git a/client/test/golden/goldens/products_archive_c.png b/client/test/golden/goldens/products_archive_c.png new file mode 100644 index 0000000..accc8b3 Binary files /dev/null and b/client/test/golden/goldens/products_archive_c.png differ diff --git a/client/test/golden/products_archive_golden_test.dart b/client/test/golden/products_archive_golden_test.dart new file mode 100644 index 0000000..dc7fcf0 --- /dev/null +++ b/client/test/golden/products_archive_golden_test.dart @@ -0,0 +1,59 @@ +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/product.dart'; +import 'package:jiu_client/providers/product_provider.dart'; +import 'package:jiu_client/screens/products/products_screen.dart'; + +import '../support/golden_harness.dart'; + +/// design-distill 阶段4:基础数据「商品档案」全量商品表 golden × 三主题。 +/// 锁住原型头牌 tab(商品 name+code / 系列 / 规格 / 分类 / 单位 / 默认成本价)。 +/// 更新基准: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), + Product( + id: 2, code: 'WLY-PW-500', name: '五粮液 普五 52°', unit: '件', + series: '普五', spec: '500ml×6', categoryName: '浓香', purchasePrice: 1050), + Product( + id: 3, code: 'JNC-SJ-500', name: '剑南春 水晶剑', unit: '件', + series: '水晶剑', spec: '500ml×6', categoryName: '浓香', purchasePrice: 438), +]; + +class _FakeProductList extends ProductListNotifier { + @override + Future> build() async => + const PageResult(data: _products, total: 3, page: 1, pageSize: 20); + @override + void setPage(int page) {} + @override + void setPageSize(int pageSize) {} + @override + void setKeyword(String keyword) {} + @override + void setCategoryId(int? categoryId) {} + @override + void reload() {} +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + + goldenAcrossThemes( + 'products 商品档案 tab', + goldenPrefix: 'products_archive', + child: () => const Scaffold(body: ProductsScreen()), + overrides: () => [ + productListProvider.overrideWith(() => _FakeProductList()), + isReadonlyProvider.overrideWithValue(false), + ], + logical: const Size(1280, 760), + ); +}