From 0d17533120b32bf24dc114371d7718c83400fc98 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 8 Jun 2026 08:06:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(client):=20=E5=9F=BA=E7=A1=80=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=AE=A1=E7=90=86=E6=96=B0=E5=A2=9E=E4=BA=A7=E5=9C=B0?= =?UTF-8?q?/=E4=BF=9D=E8=B4=A8=E6=9C=9F/=E5=82=A8=E5=AD=98=E6=96=B9?= =?UTF-8?q?=E5=BC=8F/=E6=8F=8F=E8=BF=B0=E6=96=87=E6=A1=A3=204=20=E4=B8=AA?= =?UTF-8?q?=E5=AD=97=E5=85=B8=E7=BB=B4=E6=8A=A4=20Tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../lib/screens/products/products_screen.dart | 382 +++++++++++++++++- 1 file changed, 379 insertions(+), 3 deletions(-) diff --git a/client/lib/screens/products/products_screen.dart b/client/lib/screens/products/products_screen.dart index e6b9710..a412e8c 100644 --- a/client/lib/screens/products/products_screen.dart +++ b/client/lib/screens/products/products_screen.dart @@ -22,16 +22,28 @@ class _ProductsScreenState extends ConsumerState { 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 { 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 { } } + // ── 产地 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 _showDescDocDialog({ + required String title, + required Future Function(Map) onSave, + Map? 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(); + + 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 = { + '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() {