feat: 入库单/商品详情展示产地保质期储存方式,商品详情显示建议零售价
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -165,6 +165,7 @@ func (h *ProductHandler) Detail(c *gin.Context) {
|
|||||||
var product model.Product
|
var product model.Product
|
||||||
if err := h.db.Where("id = ? AND shop_id = ? AND deleted_at IS NULL", id, shopID).
|
if err := h.db.Where("id = ? AND shop_id = ? AND deleted_at IS NULL", id, shopID).
|
||||||
Preload("Category").Preload("Images").
|
Preload("Category").Preload("Images").
|
||||||
|
Preload("Origin").Preload("ShelfLife").Preload("Storage").
|
||||||
First(&product).Error; err != nil {
|
First(&product).Error; err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ func (h *StockInHandler) List(c *gin.Context) {
|
|||||||
func (h *StockInHandler) Get(c *gin.Context) {
|
func (h *StockInHandler) Get(c *gin.Context) {
|
||||||
shopID := middleware.GetShopID(c)
|
shopID := middleware.GetShopID(c)
|
||||||
var order model.StockInOrder
|
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).
|
Where("id = ? AND shop_id = ? AND deleted_at IS NULL", c.Param("id"), shopID).
|
||||||
First(&order).Error; err != nil {
|
First(&order).Error; err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ class Product {
|
|||||||
final String? remark;
|
final String? remark;
|
||||||
final Map<String, dynamic>? customFields;
|
final Map<String, dynamic>? customFields;
|
||||||
final List<ProductImage> images;
|
final List<ProductImage> images;
|
||||||
|
// 产地/保质期/储存方式名称(Detail 接口 Preload 后填充)
|
||||||
|
final String? originName;
|
||||||
|
final String? shelfLifeName;
|
||||||
|
final String? storageName;
|
||||||
|
|
||||||
const Product({
|
const Product({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -37,6 +41,9 @@ class Product {
|
|||||||
this.remark,
|
this.remark,
|
||||||
this.customFields,
|
this.customFields,
|
||||||
this.images = const [],
|
this.images = const [],
|
||||||
|
this.originName,
|
||||||
|
this.shelfLifeName,
|
||||||
|
this.storageName,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Product.fromJson(Map<String, dynamic> json) => Product(
|
factory Product.fromJson(Map<String, dynamic> json) => Product(
|
||||||
@@ -68,6 +75,9 @@ class Product {
|
|||||||
?.map((e) => ProductImage.fromJson(e as Map<String, dynamic>))
|
?.map((e) => ProductImage.fromJson(e as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[],
|
[],
|
||||||
|
originName: (json['origin'] as Map<String, dynamic>?)?['name'] as String?,
|
||||||
|
shelfLifeName: (json['shelf_life'] as Map<String, dynamic>?)?['name'] as String?,
|
||||||
|
storageName: (json['storage'] as Map<String, dynamic>?)?['name'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ class StockInItem {
|
|||||||
final String? productSeries;
|
final String? productSeries;
|
||||||
final String? productSpec;
|
final String? productSpec;
|
||||||
final String? productUnit;
|
final String? productUnit;
|
||||||
|
final String? productOrigin;
|
||||||
|
final String? productShelfLife;
|
||||||
|
final String? productStorage;
|
||||||
|
|
||||||
const StockInItem({
|
const StockInItem({
|
||||||
this.orderId,
|
this.orderId,
|
||||||
@@ -26,6 +29,9 @@ class StockInItem {
|
|||||||
this.productSeries,
|
this.productSeries,
|
||||||
this.productSpec,
|
this.productSpec,
|
||||||
this.productUnit,
|
this.productUnit,
|
||||||
|
this.productOrigin,
|
||||||
|
this.productShelfLife,
|
||||||
|
this.productStorage,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory StockInItem.fromJson(Map<String, dynamic> json) => StockInItem(
|
factory StockInItem.fromJson(Map<String, dynamic> json) => StockInItem(
|
||||||
@@ -43,6 +49,12 @@ class StockInItem {
|
|||||||
productSeries: (json['product'] as Map<String, dynamic>?)?['series'] as String?,
|
productSeries: (json['product'] as Map<String, dynamic>?)?['series'] as String?,
|
||||||
productSpec: (json['product'] as Map<String, dynamic>?)?['spec'] as String?,
|
productSpec: (json['product'] as Map<String, dynamic>?)?['spec'] as String?,
|
||||||
productUnit: (json['product'] as Map<String, dynamic>?)?['unit'] as String?,
|
productUnit: (json['product'] as Map<String, dynamic>?)?['unit'] as String?,
|
||||||
|
productOrigin: ((json['product'] as Map<String, dynamic>?)?['origin']
|
||||||
|
as Map<String, dynamic>?)?['name'] as String?,
|
||||||
|
productShelfLife: ((json['product'] as Map<String, dynamic>?)?['shelf_life']
|
||||||
|
as Map<String, dynamic>?)?['name'] as String?,
|
||||||
|
productStorage: ((json['product'] as Map<String, dynamic>?)?['storage']
|
||||||
|
as Map<String, dynamic>?)?['name'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
|
|||||||
@@ -269,6 +269,10 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
|||||||
_buildImageSection(p),
|
_buildImageSection(p),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildInfoSection(p),
|
_buildInfoSection(p),
|
||||||
|
if (p.salePrice != null && p.salePrice! > 0) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildPriceSection(p),
|
||||||
|
],
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildDescSection(),
|
_buildDescSection(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
@@ -350,7 +354,12 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
|||||||
SizedBox(width: 200, child: row('品牌', p.brand?.isEmpty ?? true ? '-' : p.brand!)),
|
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.unit.isEmpty ? '-' : p.unit)),
|
||||||
SizedBox(width: 200, child: row('进价', p.purchasePrice != null ? '¥${p.purchasePrice!.toStringAsFixed(2)}' : '-')),
|
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)
|
if (p.barcode?.isNotEmpty ?? false)
|
||||||
SizedBox(width: 200, child: row('条码', p.barcode!)),
|
SizedBox(width: 200, child: row('条码', p.barcode!)),
|
||||||
],
|
],
|
||||||
@@ -359,6 +368,35 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
Widget _buildDescSection() {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
|||||||
@@ -813,13 +813,34 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
|
|||||||
...o.items.asMap().entries.map((e) {
|
...o.items.asMap().entries.map((e) {
|
||||||
final i = e.key;
|
final i = e.key;
|
||||||
final item = e.value;
|
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(
|
return TableRow(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: i.isEven ? Colors.white : const Color(0xFFFAFAFA)),
|
color: i.isEven ? Colors.white : const Color(0xFFFAFAFA)),
|
||||||
children: [
|
children: [
|
||||||
_TableCell('${i + 1}'),
|
_TableCell('${i + 1}'),
|
||||||
_TableCell(item.productCode ?? '-'),
|
_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.productSeries ?? '-'),
|
||||||
_TableCell(item.productSpec ?? '-'),
|
_TableCell(item.productSpec ?? '-'),
|
||||||
_TableCell(item.quantity.toStringAsFixed(3)),
|
_TableCell(item.quantity.toStringAsFixed(3)),
|
||||||
|
|||||||
Reference in New Issue
Block a user