feat(client): 基础数据管理新增产地/保质期/储存方式/描述文档 4 个字典维护 Tab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user