feat: 财务结清、酒行信息、库存备注编辑、标签溯源、入库必填校验
后端 - 新增 shop handler:GET/PUT /shop/info(管理员权限) - 新增 finance CloseByRef:按单据 ref_type+ref_id 结清账款 - 新增 inventory UpdateRemark:PUT /inventory/:id/remark - 入库/出库审批自动生成财务应付/应收记录(去除金额>0限制) - 种子数据 S001-S003 补充真实门店信息 前端 - 设置页新增「酒行信息」Tab,管理员可编辑门店名称/地址/电话/负责人 - 入库单列表新增结清按钮(含确认弹窗),出库单同步 - 入库表单:规格、系列、生产日期、供应商、商品名称改为提交必填 - 入库/出库列表新增入库时间、出库时间、创建时间列 - 商品标签标题改为读取 shop 表门店名,扫码文案改为「扫码溯源 · TRACE」 - 标签页脚显示门店地址和电话(从 API 读取,不再依赖编译时 dart-define) - 库存备注支持点击编辑,超4字截断显示+Hover展示全文 - ApiClient 新增 patch() 方法(已改用 PUT 规避 CORS) 文档 - 新增 docs/user-manual.md 完整用户操作手册(12章) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import '../../core/utils/dialog_util.dart';
|
||||
import 'dart:typed_data';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -10,6 +11,7 @@ import '../../core/theme/app_theme.dart';
|
||||
import '../../models/product.dart';
|
||||
import '../../models/product_image.dart';
|
||||
import '../../providers/product_provider.dart';
|
||||
import '../../providers/shop_provider.dart' show shopInfoProvider;
|
||||
|
||||
class ProductDetailScreen extends ConsumerStatefulWidget {
|
||||
final int productId;
|
||||
@@ -151,7 +153,7 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
||||
}
|
||||
|
||||
void _showQRCode() {
|
||||
showDialog(
|
||||
showAppDialog(
|
||||
context: context,
|
||||
builder: (_) => _QRCodeDialog(productId: widget.productId, product: _product!),
|
||||
);
|
||||
@@ -528,12 +530,16 @@ class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> {
|
||||
setState(() { _printing = true; _printStatus = '正在打印...'; });
|
||||
try {
|
||||
final p = widget.product;
|
||||
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
|
||||
await printProductLabel(
|
||||
qrBytes: _bytes!,
|
||||
name: p.name,
|
||||
code: p.code,
|
||||
spec: p.spec,
|
||||
series: p.series,
|
||||
shopName: shopInfo?.name ?? '',
|
||||
shopAddress: shopInfo?.address ?? '',
|
||||
shopPhone: shopInfo?.phone ?? '',
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import '../../core/utils/dialog_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
@@ -72,7 +73,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
searchCtrl: _nameSearchCtrl,
|
||||
hint: '搜索名称/编号',
|
||||
onSearchChanged: () => setState(() => _namePage = 1),
|
||||
onAdd: () => _showCreateDialog(
|
||||
onAdd: () => _showOptionDialog(
|
||||
title: '新建商品名称',
|
||||
hasQuantity: false,
|
||||
onSave: (data) => ref.read(productNameListProvider.notifier).create(data),
|
||||
@@ -101,10 +102,18 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
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(_deleteButton(() => _confirmDelete(
|
||||
'删除名称「${it.name}」?',
|
||||
() => ref.read(productNameListProvider.notifier).delete(it.id),
|
||||
))),
|
||||
DataCell(_actionButtons(
|
||||
onEdit: () => _showOptionDialog(
|
||||
title: '编辑商品名称',
|
||||
hasQuantity: false,
|
||||
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
|
||||
onSave: (data) => ref.read(productNameListProvider.notifier).updateItem(it.id, data),
|
||||
),
|
||||
onDelete: () => _confirmDelete(
|
||||
'删除名称「${it.name}」?',
|
||||
() => ref.read(productNameListProvider.notifier).delete(it.id),
|
||||
),
|
||||
)),
|
||||
])).toList(),
|
||||
);
|
||||
},
|
||||
@@ -136,7 +145,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
searchCtrl: _seriesSearchCtrl,
|
||||
hint: '搜索系列/编号',
|
||||
onSearchChanged: () => setState(() => _seriesPage = 1),
|
||||
onAdd: () => _showCreateDialog(
|
||||
onAdd: () => _showOptionDialog(
|
||||
title: '新建系列',
|
||||
hasQuantity: false,
|
||||
onSave: (data) => ref.read(productSeriesListProvider.notifier).create(data),
|
||||
@@ -165,10 +174,18 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
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(_deleteButton(() => _confirmDelete(
|
||||
'删除系列「${it.name}」?',
|
||||
() => ref.read(productSeriesListProvider.notifier).delete(it.id),
|
||||
))),
|
||||
DataCell(_actionButtons(
|
||||
onEdit: () => _showOptionDialog(
|
||||
title: '编辑系列',
|
||||
hasQuantity: false,
|
||||
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
|
||||
onSave: (data) => ref.read(productSeriesListProvider.notifier).updateItem(it.id, data),
|
||||
),
|
||||
onDelete: () => _confirmDelete(
|
||||
'删除系列「${it.name}」?',
|
||||
() => ref.read(productSeriesListProvider.notifier).delete(it.id),
|
||||
),
|
||||
)),
|
||||
])).toList(),
|
||||
);
|
||||
},
|
||||
@@ -200,7 +217,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
searchCtrl: _specSearchCtrl,
|
||||
hint: '搜索规格/编号',
|
||||
onSearchChanged: () => setState(() => _specPage = 1),
|
||||
onAdd: () => _showCreateDialog(
|
||||
onAdd: () => _showOptionDialog(
|
||||
title: '新建规格',
|
||||
hasQuantity: true,
|
||||
onSave: (data) => ref.read(productSpecListProvider.notifier).create(data),
|
||||
@@ -232,10 +249,18 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
DataCell(Text(it.name, style: const TextStyle(fontWeight: FontWeight.w500))),
|
||||
DataCell(Text(it.quantity > 0 ? '${it.quantity}' : '-')),
|
||||
DataCell(Text(it.remark ?? '-')),
|
||||
DataCell(_deleteButton(() => _confirmDelete(
|
||||
'删除规格「${it.name}」?',
|
||||
() => ref.read(productSpecListProvider.notifier).delete(it.id),
|
||||
))),
|
||||
DataCell(_actionButtons(
|
||||
onEdit: () => _showOptionDialog(
|
||||
title: '编辑规格',
|
||||
hasQuantity: true,
|
||||
initial: {'code': it.code ?? '', 'name': it.name, 'quantity': it.quantity, 'remark': it.remark ?? ''},
|
||||
onSave: (data) => ref.read(productSpecListProvider.notifier).updateItem(it.id, data),
|
||||
),
|
||||
onDelete: () => _confirmDelete(
|
||||
'删除规格「${it.name}」?',
|
||||
() => ref.read(productSpecListProvider.notifier).delete(it.id),
|
||||
),
|
||||
)),
|
||||
])).toList(),
|
||||
);
|
||||
},
|
||||
@@ -296,10 +321,19 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _deleteButton(VoidCallback onTap) {
|
||||
return TextButton(
|
||||
onPressed: onTap,
|
||||
child: const Text('删除', style: TextStyle(fontSize: 12, color: AppTheme.danger)),
|
||||
Widget _actionButtons({required VoidCallback onEdit, required VoidCallback onDelete}) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: onEdit,
|
||||
child: const Text('编辑', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: onDelete,
|
||||
child: const Text('删除', style: TextStyle(fontSize: 12, color: AppTheme.danger)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -332,18 +366,22 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showCreateDialog({
|
||||
Future<void> _showOptionDialog({
|
||||
required String title,
|
||||
required bool hasQuantity,
|
||||
required Future<void> Function(Map<String, dynamic>) onSave,
|
||||
Map<String, dynamic>? initial,
|
||||
}) async {
|
||||
final codeCtrl = TextEditingController();
|
||||
final nameCtrl = TextEditingController();
|
||||
final quantityCtrl = TextEditingController();
|
||||
final remarkCtrl = TextEditingController();
|
||||
final codeCtrl = TextEditingController(text: initial?['code'] as String? ?? '');
|
||||
final nameCtrl = TextEditingController(text: initial?['name'] as String? ?? '');
|
||||
final quantityCtrl = TextEditingController(
|
||||
text: initial != null && (initial['quantity'] as int? ?? 0) > 0
|
||||
? '${initial['quantity']}'
|
||||
: '');
|
||||
final remarkCtrl = TextEditingController(text: initial?['remark'] as String? ?? '');
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
await showDialog(
|
||||
await showAppDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(title),
|
||||
|
||||
Reference in New Issue
Block a user