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:
@@ -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> {
|
||||
);
|
||||
}
|
||||
|
||||
// ── 香型 tab(ProductCategory,仅名称) ──────────────────────
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user