feat(client): 商品基础数据加香型字典 tab(接后端 categories CRUD)

新增 ProductCategoryOption 模型 + repo(list/create/update/deleteCategory)+
provider(productCategoryListProvider),基础数据页加「香型」tab(增删改查、
搜索、导出,仅名称字段)。接通后端 /product-options/categories。
(原型 5-tab 整合是更大 IA 任务,本次先把香型管理补齐可用。)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
This commit is contained in:
wangjia
2026-06-25 15:42:23 +08:00
parent 355ac0ebc3
commit 38e2ea1c6c
4 changed files with 210 additions and 0 deletions
@@ -5,6 +5,7 @@ import '../../core/responsive/responsive.dart';
import '../../core/theme/context_tokens.dart';
import '../../models/warehouse.dart';
import '../../providers/product_option_provider.dart';
import '../../models/product_option.dart' show ProductCategoryOption;
import '../../providers/warehouse_provider.dart';
import '../../providers/shop_provider.dart';
import '../../widgets/data_table_card.dart';
@@ -31,6 +32,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
final _shelfLifeSearchCtrl = TextEditingController();
final _storageSearchCtrl = TextEditingController();
final _descDocSearchCtrl = TextEditingController();
final _categorySearchCtrl = TextEditingController();
int _namePage = 1;
int _namePageSize = 20;
@@ -46,6 +48,8 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
int _storagePageSize = 20;
int _descDocPage = 1;
int _descDocPageSize = 20;
int _categoryPage = 1;
int _categoryPageSize = 20;
@override
void dispose() {
@@ -56,6 +60,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
_shelfLifeSearchCtrl.dispose();
_storageSearchCtrl.dispose();
_descDocSearchCtrl.dispose();
_categorySearchCtrl.dispose();
super.dispose();
}
@@ -67,6 +72,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
Tab(text: '商品名称'),
Tab(text: '系列'),
Tab(text: '规格'),
Tab(text: '香型'),
Tab(text: '产地'),
Tab(text: '保质期'),
Tab(text: '储存方式'),
@@ -77,6 +83,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
_buildNameTab(),
_buildSeriesTab(),
_buildSpecTab(),
_buildCategoryTab(),
_buildOriginTab(),
_buildShelfLifeTab(),
_buildStorageTab(),
@@ -356,6 +363,114 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
);
}
// ── 香型 tabProductCategory,仅名称) ──────────────────────
Widget _buildCategoryTab() {
final async = ref.watch(productCategoryListProvider);
return async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => _buildError(
() => ref.read(productCategoryListProvider.notifier).reload()),
data: (items) {
final keyword = _categorySearchCtrl.text.toLowerCase();
final filtered = keyword.isEmpty
? items
: items
.where((it) => it.name.toLowerCase().contains(keyword))
.toList();
final paged = filtered
.skip((_categoryPage - 1) * _categoryPageSize)
.take(_categoryPageSize)
.toList();
final notifier = ref.read(productCategoryListProvider.notifier);
void edit(ProductCategoryOption it) => _showOptionDialog(
title: '编辑香型',
hasQuantity: false,
initial: {'name': it.name},
onSave: (data) =>
notifier.updateItem(it.id, {'name': data['name']}),
);
List<Widget> rowActions(ProductCategoryOption it) =>
WriteGuard.isReadonly(ref)
? const []
: [
WriteGuard(
child: TextButton(
onPressed: () => edit(it),
child: const Text('编辑',
style: TextStyle(fontSize: 12)),
),
),
WriteGuard(
child: TextButton(
onPressed: () => _confirmDelete(
'删除香型「${it.name}」?',
() => notifier.delete(it.id),
),
child: Text('删除',
style: TextStyle(
fontSize: 12, color: context.tokens.danger)),
),
),
];
return DataTableCard(
totalCount: filtered.length,
page: _categoryPage,
pageSize: _categoryPageSize,
onPageChanged: (p) => setState(() => _categoryPage = p),
onPageSizeChanged: (s) => setState(() {
_categoryPageSize = s;
_categoryPage = 1;
}),
toolbar: _buildToolbar(
searchCtrl: _categorySearchCtrl,
hint: '搜索香型',
onSearchChanged: () => setState(() => _categoryPage = 1),
onAdd: () => _showOptionDialog(
title: '新建香型',
hasQuantity: false,
onSave: (data) => notifier.create({'name': data['name']}),
),
onExport: () => exportExcel(
filename: '商品香型',
headers: ['香型名称'],
rows: items.map((it) => [it.name]).toList(),
),
),
mobileCards: paged
.map((it) => MobileListCard(
title: Text(it.name),
actions: rowActions(it).isEmpty ? null : rowActions(it),
))
.toList(),
columns: const [
DataColumn(label: Text('香型名称')),
DataColumn(label: Text('操作')),
],
rows: paged.isEmpty
? [
DataRow(cells: [
DataCell(Text('暂无数据',
style: TextStyle(color: context.tokens.muted))),
const DataCell(SizedBox()),
])
]
: paged
.map((it) => DataRow(cells: [
DataCell(Text(it.name,
style:
const TextStyle(fontWeight: FontWeight.w500))),
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: rowActions(it))),
]))
.toList(),
);
},
);
}
// ── 规格 tab ──────────────────────────────────────────────
Widget _buildSpecTab() {