feat(client): 基础数据管理新增产地/保质期/储存方式/描述文档 4 个字典维护 Tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-08 08:06:21 +08:00
parent 4e64e5eb47
commit 0d17533120
@@ -22,16 +22,28 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
final _nameSearchCtrl = TextEditingController();
final _seriesSearchCtrl = TextEditingController();
final _specSearchCtrl = TextEditingController();
final _originSearchCtrl = TextEditingController();
final _shelfLifeSearchCtrl = TextEditingController();
final _storageSearchCtrl = TextEditingController();
final _descDocSearchCtrl = TextEditingController();
int _namePage = 1; int _namePageSize = 20;
int _seriesPage = 1; int _seriesPageSize = 20;
int _specPage = 1; int _specPageSize = 20;
int _namePage = 1; int _namePageSize = 20;
int _seriesPage = 1; int _seriesPageSize = 20;
int _specPage = 1; int _specPageSize = 20;
int _originPage = 1; int _originPageSize = 20;
int _shelfLifePage = 1; int _shelfLifePageSize = 20;
int _storagePage = 1; int _storagePageSize = 20;
int _descDocPage = 1; int _descDocPageSize = 20;
@override
void dispose() {
_nameSearchCtrl.dispose();
_seriesSearchCtrl.dispose();
_specSearchCtrl.dispose();
_originSearchCtrl.dispose();
_shelfLifeSearchCtrl.dispose();
_storageSearchCtrl.dispose();
_descDocSearchCtrl.dispose();
super.dispose();
}
@@ -43,12 +55,20 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
Tab(text: '商品名称'),
Tab(text: '系列'),
Tab(text: '规格'),
Tab(text: '产地'),
Tab(text: '保质期'),
Tab(text: '储存方式'),
Tab(text: '描述文档'),
Tab(text: '仓库'),
],
tabViews: [
_buildNameTab(),
_buildSeriesTab(),
_buildSpecTab(),
_buildOriginTab(),
_buildShelfLifeTab(),
_buildStorageTab(),
_buildDescDocTab(),
_buildWarehousesTab(),
],
);
@@ -455,6 +475,362 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
}
}
// ── 产地 tab ──────────────────────────────────────────────
Widget _buildOriginTab() {
final async = ref.watch(productOriginListProvider);
return async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => _buildError(() => ref.read(productOriginListProvider.notifier).reload()),
data: (items) {
final keyword = _originSearchCtrl.text.toLowerCase();
final filtered = keyword.isEmpty
? items
: items.where((it) => it.name.toLowerCase().contains(keyword) ||
(it.code?.toLowerCase().contains(keyword) ?? false)).toList();
final paged = filtered.skip((_originPage - 1) * _originPageSize).take(_originPageSize).toList();
return DataTableCard(
totalCount: filtered.length,
page: _originPage,
pageSize: _originPageSize,
onPageChanged: (p) => setState(() => _originPage = p),
onPageSizeChanged: (s) => setState(() { _originPageSize = s; _originPage = 1; }),
toolbar: _buildToolbar(
searchCtrl: _originSearchCtrl,
hint: '搜索产地/编号',
onSearchChanged: () => setState(() => _originPage = 1),
onAdd: () => _showOptionDialog(
title: '新建产地',
hasQuantity: false,
onSave: (data) => ref.read(productOriginListProvider.notifier).create(data),
),
onExport: () => exportExcel(
filename: '产地',
headers: ['编号', '名称', '备注'],
rows: items.map((it) => [it.code ?? '', it.name, it.remark ?? '']).toList(),
),
),
mobileCards: paged.map((it) => _optionCard(
code: it.code, name: it.name, remark: it.remark,
onEdit: () => _showOptionDialog(
title: '编辑产地', hasQuantity: false,
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productOriginListProvider.notifier).updateItem(it.id, data),
),
onDelete: () => _confirmDelete('删除产地「${it.name}」?',
() => ref.read(productOriginListProvider.notifier).delete(it.id)),
)).toList(),
columns: const [DataColumn(label: Text('编号')), DataColumn(label: Text('名称')), DataColumn(label: Text('备注')), DataColumn(label: Text('操作'))],
rows: paged.isEmpty
? [DataRow(cells: [const DataCell(SizedBox()), const DataCell(Text('暂无数据', style: TextStyle(color: AppTheme.textSecondary))), const DataCell(SizedBox()), const DataCell(SizedBox())])]
: paged.map((it) => DataRow(cells: [
DataCell(Text(it.code ?? '-', style: const TextStyle(fontFamily: 'monospace', fontSize: 12, color: AppTheme.textSecondary))),
DataCell(Text(it.name, style: const TextStyle(fontWeight: FontWeight.w500))),
DataCell(Text(it.remark ?? '-')),
DataCell(_actionButtons(
onEdit: () => _showOptionDialog(
title: '编辑产地', hasQuantity: false,
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productOriginListProvider.notifier).updateItem(it.id, data),
),
onDelete: () => _confirmDelete('删除产地「${it.name}」?',
() => ref.read(productOriginListProvider.notifier).delete(it.id)),
)),
])).toList(),
);
},
);
}
// ── 保质期 tab ─────────────────────────────────────────────
Widget _buildShelfLifeTab() {
final async = ref.watch(productShelfLifeListProvider);
return async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => _buildError(() => ref.read(productShelfLifeListProvider.notifier).reload()),
data: (items) {
final keyword = _shelfLifeSearchCtrl.text.toLowerCase();
final filtered = keyword.isEmpty
? items
: items.where((it) => it.name.toLowerCase().contains(keyword) ||
(it.code?.toLowerCase().contains(keyword) ?? false)).toList();
final paged = filtered.skip((_shelfLifePage - 1) * _shelfLifePageSize).take(_shelfLifePageSize).toList();
return DataTableCard(
totalCount: filtered.length,
page: _shelfLifePage,
pageSize: _shelfLifePageSize,
onPageChanged: (p) => setState(() => _shelfLifePage = p),
onPageSizeChanged: (s) => setState(() { _shelfLifePageSize = s; _shelfLifePage = 1; }),
toolbar: _buildToolbar(
searchCtrl: _shelfLifeSearchCtrl,
hint: '搜索保质期/编号',
onSearchChanged: () => setState(() => _shelfLifePage = 1),
onAdd: () => _showOptionDialog(
title: '新建保质期',
hasQuantity: false,
onSave: (data) => ref.read(productShelfLifeListProvider.notifier).create(data),
),
onExport: () => exportExcel(
filename: '保质期',
headers: ['编号', '名称', '备注'],
rows: items.map((it) => [it.code ?? '', it.name, it.remark ?? '']).toList(),
),
),
mobileCards: paged.map((it) => _optionCard(
code: it.code, name: it.name, remark: it.remark,
onEdit: () => _showOptionDialog(
title: '编辑保质期', hasQuantity: false,
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productShelfLifeListProvider.notifier).updateItem(it.id, data),
),
onDelete: () => _confirmDelete('删除保质期「${it.name}」?',
() => ref.read(productShelfLifeListProvider.notifier).delete(it.id)),
)).toList(),
columns: const [DataColumn(label: Text('编号')), DataColumn(label: Text('名称')), DataColumn(label: Text('备注')), DataColumn(label: Text('操作'))],
rows: paged.isEmpty
? [DataRow(cells: [const DataCell(SizedBox()), const DataCell(Text('暂无数据', style: TextStyle(color: AppTheme.textSecondary))), const DataCell(SizedBox()), const DataCell(SizedBox())])]
: paged.map((it) => DataRow(cells: [
DataCell(Text(it.code ?? '-', style: const TextStyle(fontFamily: 'monospace', fontSize: 12, color: AppTheme.textSecondary))),
DataCell(Text(it.name, style: const TextStyle(fontWeight: FontWeight.w500))),
DataCell(Text(it.remark ?? '-')),
DataCell(_actionButtons(
onEdit: () => _showOptionDialog(
title: '编辑保质期', hasQuantity: false,
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productShelfLifeListProvider.notifier).updateItem(it.id, data),
),
onDelete: () => _confirmDelete('删除保质期「${it.name}」?',
() => ref.read(productShelfLifeListProvider.notifier).delete(it.id)),
)),
])).toList(),
);
},
);
}
// ── 储存方式 tab ───────────────────────────────────────────
Widget _buildStorageTab() {
final async = ref.watch(productStorageListProvider);
return async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => _buildError(() => ref.read(productStorageListProvider.notifier).reload()),
data: (items) {
final keyword = _storageSearchCtrl.text.toLowerCase();
final filtered = keyword.isEmpty
? items
: items.where((it) => it.name.toLowerCase().contains(keyword) ||
(it.code?.toLowerCase().contains(keyword) ?? false)).toList();
final paged = filtered.skip((_storagePage - 1) * _storagePageSize).take(_storagePageSize).toList();
return DataTableCard(
totalCount: filtered.length,
page: _storagePage,
pageSize: _storagePageSize,
onPageChanged: (p) => setState(() => _storagePage = p),
onPageSizeChanged: (s) => setState(() { _storagePageSize = s; _storagePage = 1; }),
toolbar: _buildToolbar(
searchCtrl: _storageSearchCtrl,
hint: '搜索储存方式/编号',
onSearchChanged: () => setState(() => _storagePage = 1),
onAdd: () => _showOptionDialog(
title: '新建储存方式',
hasQuantity: false,
onSave: (data) => ref.read(productStorageListProvider.notifier).create(data),
),
onExport: () => exportExcel(
filename: '储存方式',
headers: ['编号', '名称', '备注'],
rows: items.map((it) => [it.code ?? '', it.name, it.remark ?? '']).toList(),
),
),
mobileCards: paged.map((it) => _optionCard(
code: it.code, name: it.name, remark: it.remark,
onEdit: () => _showOptionDialog(
title: '编辑储存方式', hasQuantity: false,
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productStorageListProvider.notifier).updateItem(it.id, data),
),
onDelete: () => _confirmDelete('删除储存方式「${it.name}」?',
() => ref.read(productStorageListProvider.notifier).delete(it.id)),
)).toList(),
columns: const [DataColumn(label: Text('编号')), DataColumn(label: Text('名称')), DataColumn(label: Text('备注')), DataColumn(label: Text('操作'))],
rows: paged.isEmpty
? [DataRow(cells: [const DataCell(SizedBox()), const DataCell(Text('暂无数据', style: TextStyle(color: AppTheme.textSecondary))), const DataCell(SizedBox()), const DataCell(SizedBox())])]
: paged.map((it) => DataRow(cells: [
DataCell(Text(it.code ?? '-', style: const TextStyle(fontFamily: 'monospace', fontSize: 12, color: AppTheme.textSecondary))),
DataCell(Text(it.name, style: const TextStyle(fontWeight: FontWeight.w500))),
DataCell(Text(it.remark ?? '-')),
DataCell(_actionButtons(
onEdit: () => _showOptionDialog(
title: '编辑储存方式', hasQuantity: false,
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productStorageListProvider.notifier).updateItem(it.id, data),
),
onDelete: () => _confirmDelete('删除储存方式「${it.name}」?',
() => ref.read(productStorageListProvider.notifier).delete(it.id)),
)),
])).toList(),
);
},
);
}
// ── 描述文档 tab ───────────────────────────────────────────
Widget _buildDescDocTab() {
final async = ref.watch(productDescriptionDocListProvider);
return async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => _buildError(() => ref.read(productDescriptionDocListProvider.notifier).reload()),
data: (items) {
final keyword = _descDocSearchCtrl.text.toLowerCase();
final filtered = keyword.isEmpty
? items
: items.where((it) => it.title.toLowerCase().contains(keyword)).toList();
final paged = filtered.skip((_descDocPage - 1) * _descDocPageSize).take(_descDocPageSize).toList();
return DataTableCard(
totalCount: filtered.length,
page: _descDocPage,
pageSize: _descDocPageSize,
onPageChanged: (p) => setState(() => _descDocPage = p),
onPageSizeChanged: (s) => setState(() { _descDocPageSize = s; _descDocPage = 1; }),
toolbar: _buildToolbar(
searchCtrl: _descDocSearchCtrl,
hint: '搜索标题',
onSearchChanged: () => setState(() => _descDocPage = 1),
onAdd: () => _showDescDocDialog(
title: '新建描述文档',
onSave: (data) => ref.read(productDescriptionDocListProvider.notifier).create(data),
),
onExport: () => exportExcel(
filename: '描述文档',
headers: ['标题', '内容', '备注'],
rows: items.map((it) => [it.title, it.content ?? '', it.remark ?? '']).toList(),
),
),
mobileCards: paged.map((it) => MobileListCard(
title: Text(it.title),
fields: [
if (it.content?.isNotEmpty == true)
MobileCardField('内容', it.content),
if (it.remark?.isNotEmpty == true)
MobileCardField('备注', it.remark),
],
actions: [
TextButton(
onPressed: () => _showDescDocDialog(
title: '编辑描述文档',
initial: {'title': it.title, 'content': it.content ?? '', 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productDescriptionDocListProvider.notifier).updateItem(it.id, data),
),
child: const Text('编辑', style: TextStyle(fontSize: 13)),
),
TextButton(
onPressed: () => _confirmDelete('删除描述文档「${it.title}」?',
() => ref.read(productDescriptionDocListProvider.notifier).delete(it.id)),
child: const Text('删除', style: TextStyle(fontSize: 13, color: AppTheme.danger)),
),
],
)).toList(),
columns: const [DataColumn(label: Text('标题')), DataColumn(label: Text('内容预览')), DataColumn(label: Text('备注')), DataColumn(label: Text('操作'))],
rows: paged.isEmpty
? [DataRow(cells: [const DataCell(SizedBox()), const DataCell(Text('暂无数据', style: TextStyle(color: AppTheme.textSecondary))), const DataCell(SizedBox()), const DataCell(SizedBox())])]
: paged.map((it) {
final preview = (it.content ?? '').length > 30
? '${it.content!.substring(0, 30)}'
: (it.content ?? '-');
return DataRow(cells: [
DataCell(Text(it.title, style: const TextStyle(fontWeight: FontWeight.w500))),
DataCell(Text(preview, style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary))),
DataCell(Text(it.remark ?? '-')),
DataCell(_actionButtons(
onEdit: () => _showDescDocDialog(
title: '编辑描述文档',
initial: {'title': it.title, 'content': it.content ?? '', 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productDescriptionDocListProvider.notifier).updateItem(it.id, data),
),
onDelete: () => _confirmDelete('删除描述文档「${it.title}」?',
() => ref.read(productDescriptionDocListProvider.notifier).delete(it.id)),
)),
]);
}).toList(),
);
},
);
}
Future<void> _showDescDocDialog({
required String title,
required Future<void> Function(Map<String, dynamic>) onSave,
Map<String, dynamic>? initial,
}) async {
final titleCtrl = TextEditingController(text: initial?['title'] as String? ?? '');
final contentCtrl = TextEditingController(text: initial?['content'] as String? ?? '');
final remarkCtrl = TextEditingController(text: initial?['remark'] as String? ?? '');
final formKey = GlobalKey<FormState>();
await showAppDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(title),
content: SizedBox(
width: context.dialogWidth(400),
child: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: titleCtrl,
decoration: const InputDecoration(labelText: '标题 *(如:酱香型白酒)'),
validator: (v) => (v == null || v.trim().isEmpty) ? '请输入标题' : null,
),
const SizedBox(height: 12),
TextFormField(
controller: contentCtrl,
decoration: const InputDecoration(labelText: '内容(商品介绍正文)'),
maxLines: 5,
minLines: 3,
),
const SizedBox(height: 12),
TextFormField(
controller: remarkCtrl,
decoration: const InputDecoration(labelText: '备注'),
maxLines: 2,
),
],
),
),
),
actions: [
TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('取消')),
ElevatedButton(
onPressed: () async {
if (!formKey.currentState!.validate()) return;
final data = <String, dynamic>{
'title': titleCtrl.text.trim(),
if (contentCtrl.text.isNotEmpty) 'content': contentCtrl.text.trim(),
if (remarkCtrl.text.isNotEmpty) 'remark': remarkCtrl.text.trim(),
};
Navigator.of(ctx).pop();
try {
await onSave(data);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('保存失败:$e'), backgroundColor: AppTheme.danger),
);
}
}
},
child: const Text('保存'),
),
],
),
);
}
// ── 仓库 tab ──────────────────────────────────────────────
Widget _buildWarehousesTab() {