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 '../../core/utils/dialog_util.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../core/responsive/responsive.dart';
|
import '../../core/responsive/responsive.dart';
|
||||||
import '../../core/theme/context_tokens.dart';
|
import '../../core/theme/context_tokens.dart';
|
||||||
|
import '../../core/theme/app_dims.g.dart';
|
||||||
import '../../models/warehouse.dart';
|
import '../../models/warehouse.dart';
|
||||||
|
import '../../models/product.dart';
|
||||||
import '../../providers/product_option_provider.dart';
|
import '../../providers/product_option_provider.dart';
|
||||||
|
import '../../providers/product_provider.dart';
|
||||||
import '../../models/product_option.dart' show ProductCategoryOption;
|
import '../../models/product_option.dart' show ProductCategoryOption;
|
||||||
import '../../providers/warehouse_provider.dart';
|
import '../../providers/warehouse_provider.dart';
|
||||||
import '../../providers/shop_provider.dart';
|
import '../../providers/shop_provider.dart';
|
||||||
@@ -25,6 +29,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
// 「设为默认」的乐观覆盖:点一下立刻变色,不等后端回包(key→选项 id)。
|
// 「设为默认」的乐观覆盖:点一下立刻变色,不等后端回包(key→选项 id)。
|
||||||
final Map<String, int> _optDefaults = {};
|
final Map<String, int> _optDefaults = {};
|
||||||
|
|
||||||
|
final _archiveSearchCtrl = TextEditingController();
|
||||||
final _nameSearchCtrl = TextEditingController();
|
final _nameSearchCtrl = TextEditingController();
|
||||||
final _seriesSearchCtrl = TextEditingController();
|
final _seriesSearchCtrl = TextEditingController();
|
||||||
final _specSearchCtrl = TextEditingController();
|
final _specSearchCtrl = TextEditingController();
|
||||||
@@ -61,6 +66,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
_storageSearchCtrl.dispose();
|
_storageSearchCtrl.dispose();
|
||||||
_descDocSearchCtrl.dispose();
|
_descDocSearchCtrl.dispose();
|
||||||
_categorySearchCtrl.dispose();
|
_categorySearchCtrl.dispose();
|
||||||
|
_archiveSearchCtrl.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,31 +74,167 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PageScaffold(
|
return PageScaffold(
|
||||||
title: '基础数据',
|
title: '基础数据',
|
||||||
|
// 原型 5 个 tab(商品档案/商品介绍/系列/规格/香型)置前;
|
||||||
|
// 其余字典(商品名称/产地/保质期/储存/仓库)保留在后,不删管理入口。
|
||||||
tabs: const [
|
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: '产地'),
|
||||||
Tab(text: '保质期'),
|
Tab(text: '保质期'),
|
||||||
Tab(text: '储存方式'),
|
Tab(text: '储存方式'),
|
||||||
Tab(text: '描述文档'),
|
|
||||||
Tab(text: '仓库'),
|
Tab(text: '仓库'),
|
||||||
],
|
],
|
||||||
tabViews: [
|
tabViews: [
|
||||||
_buildNameTab(),
|
_buildArchiveTab(),
|
||||||
|
_buildDescDocTab(),
|
||||||
_buildSeriesTab(),
|
_buildSeriesTab(),
|
||||||
_buildSpecTab(),
|
_buildSpecTab(),
|
||||||
_buildCategoryTab(),
|
_buildCategoryTab(),
|
||||||
|
_buildNameTab(),
|
||||||
_buildOriginTab(),
|
_buildOriginTab(),
|
||||||
_buildShelfLifeTab(),
|
_buildShelfLifeTab(),
|
||||||
_buildStorageTab(),
|
_buildStorageTab(),
|
||||||
_buildDescDocTab(),
|
|
||||||
_buildWarehousesTab(),
|
_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 ───────────────────────────────────────────
|
// ── 商品名称 tab ───────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildNameTab() {
|
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