diff --git a/backend/internal/handler/product.go b/backend/internal/handler/product.go index 8a5f334..2ed8858 100644 --- a/backend/internal/handler/product.go +++ b/backend/internal/handler/product.go @@ -165,6 +165,7 @@ func (h *ProductHandler) Detail(c *gin.Context) { var product model.Product if err := h.db.Where("id = ? AND shop_id = ? AND deleted_at IS NULL", id, shopID). Preload("Category").Preload("Images"). + Preload("Origin").Preload("ShelfLife").Preload("Storage"). First(&product).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return diff --git a/backend/internal/handler/stock_in.go b/backend/internal/handler/stock_in.go index 810d3e4..8797891 100644 --- a/backend/internal/handler/stock_in.go +++ b/backend/internal/handler/stock_in.go @@ -62,7 +62,9 @@ func (h *StockInHandler) List(c *gin.Context) { func (h *StockInHandler) Get(c *gin.Context) { shopID := middleware.GetShopID(c) var order model.StockInOrder - if err := h.db.Preload("Items.Product").Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer"). + if err := h.db.Preload("Items.Product").Preload("Items.Product.Origin"). + Preload("Items.Product.ShelfLife").Preload("Items.Product.Storage"). + Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer"). Where("id = ? AND shop_id = ? AND deleted_at IS NULL", c.Param("id"), shopID). First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) diff --git a/client/lib/models/product.dart b/client/lib/models/product.dart index bab2c1f..e9313ed 100644 --- a/client/lib/models/product.dart +++ b/client/lib/models/product.dart @@ -18,6 +18,10 @@ class Product { final String? remark; final Map? customFields; final List images; + // 产地/保质期/储存方式名称(Detail 接口 Preload 后填充) + final String? originName; + final String? shelfLifeName; + final String? storageName; const Product({ required this.id, @@ -37,6 +41,9 @@ class Product { this.remark, this.customFields, this.images = const [], + this.originName, + this.shelfLifeName, + this.storageName, }); factory Product.fromJson(Map json) => Product( @@ -68,6 +75,9 @@ class Product { ?.map((e) => ProductImage.fromJson(e as Map)) .toList() ?? [], + originName: (json['origin'] as Map?)?['name'] as String?, + shelfLifeName: (json['shelf_life'] as Map?)?['name'] as String?, + storageName: (json['storage'] as Map?)?['name'] as String?, ); Map toJson() => { diff --git a/client/lib/models/stock_in.dart b/client/lib/models/stock_in.dart index 892e0cd..f10c526 100644 --- a/client/lib/models/stock_in.dart +++ b/client/lib/models/stock_in.dart @@ -12,6 +12,9 @@ class StockInItem { final String? productSeries; final String? productSpec; final String? productUnit; + final String? productOrigin; + final String? productShelfLife; + final String? productStorage; const StockInItem({ this.orderId, @@ -26,6 +29,9 @@ class StockInItem { this.productSeries, this.productSpec, this.productUnit, + this.productOrigin, + this.productShelfLife, + this.productStorage, }); factory StockInItem.fromJson(Map json) => StockInItem( @@ -43,6 +49,12 @@ class StockInItem { productSeries: (json['product'] as Map?)?['series'] as String?, productSpec: (json['product'] as Map?)?['spec'] as String?, productUnit: (json['product'] as Map?)?['unit'] as String?, + productOrigin: ((json['product'] as Map?)?['origin'] + as Map?)?['name'] as String?, + productShelfLife: ((json['product'] as Map?)?['shelf_life'] + as Map?)?['name'] as String?, + productStorage: ((json['product'] as Map?)?['storage'] + as Map?)?['name'] as String?, ); Map toJson() => { diff --git a/client/lib/screens/products/product_detail_screen.dart b/client/lib/screens/products/product_detail_screen.dart index feec6ad..a878fc9 100644 --- a/client/lib/screens/products/product_detail_screen.dart +++ b/client/lib/screens/products/product_detail_screen.dart @@ -269,6 +269,10 @@ class _ProductDetailScreenState extends ConsumerState { _buildImageSection(p), const SizedBox(height: 20), _buildInfoSection(p), + if (p.salePrice != null && p.salePrice! > 0) ...[ + const SizedBox(height: 20), + _buildPriceSection(p), + ], const SizedBox(height: 20), _buildDescSection(), const SizedBox(height: 20), @@ -350,7 +354,12 @@ class _ProductDetailScreenState extends ConsumerState { SizedBox(width: 200, child: row('品牌', p.brand?.isEmpty ?? true ? '-' : p.brand!)), SizedBox(width: 200, child: row('单位', p.unit.isEmpty ? '-' : p.unit)), SizedBox(width: 200, child: row('进价', p.purchasePrice != null ? '¥${p.purchasePrice!.toStringAsFixed(2)}' : '-')), - SizedBox(width: 200, child: row('售价', p.salePrice != null ? '¥${p.salePrice!.toStringAsFixed(2)}' : '-')), + if (p.originName?.isNotEmpty ?? false) + SizedBox(width: 200, child: row('产地', p.originName!)), + if (p.shelfLifeName?.isNotEmpty ?? false) + SizedBox(width: 200, child: row('保质期', p.shelfLifeName!)), + if (p.storageName?.isNotEmpty ?? false) + SizedBox(width: 200, child: row('储存方式', p.storageName!)), if (p.barcode?.isNotEmpty ?? false) SizedBox(width: 200, child: row('条码', p.barcode!)), ], @@ -359,6 +368,35 @@ class _ProductDetailScreenState extends ConsumerState { ); } + Widget _buildPriceSection(Product p) { + final price = p.salePrice!; + final priceStr = price % 1 == 0 + ? price.toInt().toString() + : price.toStringAsFixed(2); + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + side: BorderSide(color: AppTheme.border, width: 0.5), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + const Text('建议零售价', + style: TextStyle(fontSize: 13, color: AppTheme.textSecondary)), + const Spacer(), + Text('¥$priceStr', + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + color: AppTheme.danger)), + ], + ), + ), + ); + } + Widget _buildDescSection() { return Card( elevation: 0, diff --git a/client/lib/screens/stock_in/stock_in_list_screen.dart b/client/lib/screens/stock_in/stock_in_list_screen.dart index 9c16995..8ddd504 100644 --- a/client/lib/screens/stock_in/stock_in_list_screen.dart +++ b/client/lib/screens/stock_in/stock_in_list_screen.dart @@ -813,13 +813,34 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> { ...o.items.asMap().entries.map((e) { final i = e.key; final item = e.value; + // 产地/保质期/储存拼成一行辅助文本 + final attrs = [ + if (item.productOrigin?.isNotEmpty ?? false) item.productOrigin!, + if (item.productShelfLife?.isNotEmpty ?? false) item.productShelfLife!, + if (item.productStorage?.isNotEmpty ?? false) item.productStorage!, + ].join(' · '); return TableRow( decoration: BoxDecoration( color: i.isEven ? Colors.white : const Color(0xFFFAFAFA)), children: [ _TableCell('${i + 1}'), _TableCell(item.productCode ?? '-'), - _TableCell(item.productName ?? '-'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(item.productName ?? '-', + style: const TextStyle(fontSize: 13)), + if (attrs.isNotEmpty) + Text(attrs, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF888888))), + ], + ), + ), _TableCell(item.productSeries ?? '-'), _TableCell(item.productSpec ?? '-'), _TableCell(item.quantity.toStringAsFixed(3)),