feat(client): 基础数据加商品档案全量表 + 原型 5 tab 置前
非破坏式向原型靠拢: - 新增「商品档案」tab(productListProvider 全量商品表:商品 name+code/系列/规格/ 分类/单位/默认成本价/查看,行点进详情),原型头牌功能(此前基础数据页只管字典) - tab 重排:原型 5 个(商品档案/商品介绍/系列/规格/香型)置前;描述文档→商品介绍 - 保留 商品名称/产地/保质期/储存/仓库 字典 tab 在后(不删管理入口、不孤立数据) 整屏 golden ×三主题入回归闸。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
This commit is contained in:
@@ -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<ProductsScreen> {
|
||||
// 「设为默认」的乐观覆盖:点一下立刻变色,不等后端回包(key→选项 id)。
|
||||
final Map<String, int> _optDefaults = {};
|
||||
|
||||
final _archiveSearchCtrl = TextEditingController();
|
||||
final _nameSearchCtrl = TextEditingController();
|
||||
final _seriesSearchCtrl = TextEditingController();
|
||||
final _specSearchCtrl = TextEditingController();
|
||||
@@ -61,6 +66,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
_storageSearchCtrl.dispose();
|
||||
_descDocSearchCtrl.dispose();
|
||||
_categorySearchCtrl.dispose();
|
||||
_archiveSearchCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -68,31 +74,167 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
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() {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -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<PageResult<Product>> 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),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user