明细计算,产品详情页
This commit is contained in:
@@ -8,6 +8,7 @@ import com.gear.common.core.controller.BaseController;
|
|||||||
import com.gear.mat.domain.bo.MatProductAdditionBo;
|
import com.gear.mat.domain.bo.MatProductAdditionBo;
|
||||||
import com.gear.mat.domain.vo.MatProductAdditionVo;
|
import com.gear.mat.domain.vo.MatProductAdditionVo;
|
||||||
import com.gear.mat.service.IMatProductAdditionService;
|
import com.gear.mat.service.IMatProductAdditionService;
|
||||||
|
import lombok.Data;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -69,4 +70,19 @@ public class MatProductAdditionController extends BaseController {
|
|||||||
boolean result = productAdditionService.delProductAddition(addId);
|
boolean result = productAdditionService.delProductAddition(addId);
|
||||||
return R.ok(result);
|
return R.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Log(title = "产品属性附加表", businessType = BusinessType.UPDATE)
|
||||||
|
@PostMapping("/batchSave")
|
||||||
|
public R<Boolean> batchSave(@RequestBody BatchSaveRequest payload) {
|
||||||
|
if (payload == null) {
|
||||||
|
return R.ok(false);
|
||||||
|
}
|
||||||
|
return R.ok(productAdditionService.batchSaveProductAddition(payload.getProductId(), payload.getItems()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class BatchSaveRequest {
|
||||||
|
private Long productId;
|
||||||
|
private List<MatProductAdditionBo> items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.gear.common.enums.BusinessType;
|
|||||||
import com.gear.mat.domain.bo.MatProductLaborBo;
|
import com.gear.mat.domain.bo.MatProductLaborBo;
|
||||||
import com.gear.mat.domain.vo.MatProductLaborVo;
|
import com.gear.mat.domain.vo.MatProductLaborVo;
|
||||||
import com.gear.mat.service.IMatProductLaborService;
|
import com.gear.mat.service.IMatProductLaborService;
|
||||||
|
import lombok.Data;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -46,4 +47,19 @@ public class MatProductLaborController extends BaseController {
|
|||||||
boolean result = productLaborService.delProductLabor(laborId);
|
boolean result = productLaborService.delProductLabor(laborId);
|
||||||
return R.ok(result);
|
return R.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Log(title = "产品手动工价", businessType = BusinessType.UPDATE)
|
||||||
|
@PostMapping("/batchSave")
|
||||||
|
public R<Boolean> batchSave(@RequestBody BatchSaveRequest payload) {
|
||||||
|
if (payload == null) {
|
||||||
|
return R.ok(false);
|
||||||
|
}
|
||||||
|
return R.ok(productLaborService.batchSaveProductLabor(payload.getProductId(), payload.getItems()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class BatchSaveRequest {
|
||||||
|
private Long productId;
|
||||||
|
private List<MatProductLaborBo> items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ public class MatMaterialVo {
|
|||||||
@ExcelDictFormat(readConverterExp = "已=入库")
|
@ExcelDictFormat(readConverterExp = "已=入库")
|
||||||
private BigDecimal currentStock;
|
private BigDecimal currentStock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单价(用于成本测算:默认取最新入库单价,其次取最新采购单价)
|
||||||
|
*/
|
||||||
|
private BigDecimal unitPrice;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 备注
|
* 备注
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -44,4 +44,6 @@ public interface IMatProductAdditionService extends IService<MatProductAddition>
|
|||||||
* @return 删除结果
|
* @return 删除结果
|
||||||
*/
|
*/
|
||||||
boolean delProductAddition(Long addId);
|
boolean delProductAddition(Long addId);
|
||||||
|
|
||||||
|
boolean batchSaveProductAddition(Long productId, List<MatProductAdditionBo> items);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,6 @@ public interface IMatProductLaborService extends IService<MatProductLabor> {
|
|||||||
boolean updateProductLabor(MatProductLaborBo productLaborBo);
|
boolean updateProductLabor(MatProductLaborBo productLaborBo);
|
||||||
|
|
||||||
boolean delProductLabor(Long laborId);
|
boolean delProductLabor(Long laborId);
|
||||||
|
|
||||||
|
boolean batchSaveProductLabor(Long productId, List<MatProductLaborBo> items);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,41 @@ public class MatMaterialServiceImpl implements IMatMaterialService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public MatMaterialVo queryById(Long materialId){
|
public MatMaterialVo queryById(Long materialId){
|
||||||
return baseMapper.selectVoById(materialId);
|
MatMaterialVo vo = baseMapper.selectVoById(materialId);
|
||||||
|
if (vo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
vo.setUnitPrice(resolveUnitPrice(materialId));
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal resolveUnitPrice(Long materialId) {
|
||||||
|
QueryWrapper<MatPurchaseInDetail> inDetailQw = new QueryWrapper<>();
|
||||||
|
inDetailQw.eq("material_id", materialId);
|
||||||
|
inDetailQw.eq("del_flag", 0);
|
||||||
|
inDetailQw.isNotNull("in_price");
|
||||||
|
inDetailQw.orderByDesc("in_time");
|
||||||
|
inDetailQw.orderByDesc("create_time");
|
||||||
|
inDetailQw.last("LIMIT 1");
|
||||||
|
MatPurchaseInDetail latestInDetail = matPurchaseInDetailMapper.selectOne(inDetailQw);
|
||||||
|
if (latestInDetail != null && latestInDetail.getInPrice() != null) {
|
||||||
|
return latestInDetail.getInPrice();
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryWrapper<MatPurchase> purchaseQw = new QueryWrapper<>();
|
||||||
|
purchaseQw.eq("material_id", materialId);
|
||||||
|
purchaseQw.eq("del_flag", 0);
|
||||||
|
purchaseQw.ne("status", 3);
|
||||||
|
purchaseQw.isNotNull("purchase_price");
|
||||||
|
purchaseQw.orderByDesc("update_time");
|
||||||
|
purchaseQw.orderByDesc("create_time");
|
||||||
|
purchaseQw.last("LIMIT 1");
|
||||||
|
MatPurchase latestPurchase = matPurchaseMapper.selectOne(purchaseQw);
|
||||||
|
if (latestPurchase != null && latestPurchase.getPurchasePrice() != null) {
|
||||||
|
return latestPurchase.getPurchasePrice();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import com.gear.mat.mapper.MatProductAdditionMapper;
|
|||||||
import com.gear.mat.service.IMatProductAdditionService;
|
import com.gear.mat.service.IMatProductAdditionService;
|
||||||
import com.gear.common.utils.BeanCopyUtils;
|
import com.gear.common.utils.BeanCopyUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 产品属性附加表Service实现类
|
* 产品属性附加表Service实现类
|
||||||
@@ -51,4 +54,55 @@ public class MatProductAdditionServiceImpl extends ServiceImpl<MatProductAdditio
|
|||||||
public boolean delProductAddition(Long addId) {
|
public boolean delProductAddition(Long addId) {
|
||||||
return removeById(addId);
|
return removeById(addId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public boolean batchSaveProductAddition(Long productId, List<MatProductAdditionBo> items) {
|
||||||
|
if (productId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MatProductAddition> exist = baseMapper.selectList(new LambdaQueryWrapper<MatProductAddition>()
|
||||||
|
.select(MatProductAddition::getAddId)
|
||||||
|
.eq(MatProductAddition::getProductId, productId)
|
||||||
|
.eq(MatProductAddition::getDelFlag, 0));
|
||||||
|
|
||||||
|
Set<Long> existIds = exist.stream()
|
||||||
|
.map(MatProductAddition::getAddId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
Set<Long> incomingIds = items == null ? java.util.Collections.emptySet() : items.stream()
|
||||||
|
.map(MatProductAdditionBo::getAddId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
List<Long> deleteIds = existIds.stream()
|
||||||
|
.filter(id -> !incomingIds.contains(id))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (!deleteIds.isEmpty()) {
|
||||||
|
removeByIds(deleteIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items == null || items.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (MatProductAdditionBo item : items) {
|
||||||
|
if (item == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
MatProductAddition addition = BeanCopyUtils.copy(item, MatProductAddition.class);
|
||||||
|
if (addition == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
addition.setProductId(productId);
|
||||||
|
if (addition.getAddId() == null) {
|
||||||
|
save(addition);
|
||||||
|
} else {
|
||||||
|
updateById(addition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ import com.gear.mat.domain.vo.MatProductLaborVo;
|
|||||||
import com.gear.mat.mapper.MatProductLaborMapper;
|
import com.gear.mat.mapper.MatProductLaborMapper;
|
||||||
import com.gear.mat.service.IMatProductLaborService;
|
import com.gear.mat.service.IMatProductLaborService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class MatProductLaborServiceImpl extends ServiceImpl<MatProductLaborMapper, MatProductLabor> implements IMatProductLaborService {
|
public class MatProductLaborServiceImpl extends ServiceImpl<MatProductLaborMapper, MatProductLabor> implements IMatProductLaborService {
|
||||||
@@ -42,4 +45,55 @@ public class MatProductLaborServiceImpl extends ServiceImpl<MatProductLaborMappe
|
|||||||
public boolean delProductLabor(Long laborId) {
|
public boolean delProductLabor(Long laborId) {
|
||||||
return removeById(laborId);
|
return removeById(laborId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public boolean batchSaveProductLabor(Long productId, List<MatProductLaborBo> items) {
|
||||||
|
if (productId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MatProductLabor> exist = baseMapper.selectList(new LambdaQueryWrapper<MatProductLabor>()
|
||||||
|
.select(MatProductLabor::getLaborId)
|
||||||
|
.eq(MatProductLabor::getProductId, productId)
|
||||||
|
.eq(MatProductLabor::getDelFlag, 0));
|
||||||
|
|
||||||
|
Set<Long> existIds = exist.stream()
|
||||||
|
.map(MatProductLabor::getLaborId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
Set<Long> incomingIds = items == null ? java.util.Collections.emptySet() : items.stream()
|
||||||
|
.map(MatProductLaborBo::getLaborId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
List<Long> deleteIds = existIds.stream()
|
||||||
|
.filter(id -> !incomingIds.contains(id))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (!deleteIds.isEmpty()) {
|
||||||
|
removeByIds(deleteIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items == null || items.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (MatProductLaborBo item : items) {
|
||||||
|
if (item == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
MatProductLabor labor = BeanCopyUtils.copy(item, MatProductLabor.class);
|
||||||
|
if (labor == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
labor.setProductId(productId);
|
||||||
|
if (labor.getLaborId() == null) {
|
||||||
|
save(labor);
|
||||||
|
} else {
|
||||||
|
updateById(labor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,16 @@ public class GearWageEntryDetail extends BaseEntity {
|
|||||||
|
|
||||||
private String extraReason;
|
private String extraReason;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 明细计算金额(多行乘法合计)
|
||||||
|
*/
|
||||||
|
private BigDecimal calcAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 明细计算行JSON(两列乘法)
|
||||||
|
*/
|
||||||
|
private String calcDetail;
|
||||||
|
|
||||||
private BigDecimal totalAmount;
|
private BigDecimal totalAmount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ public class GearWageEntryDetailBo extends BaseEntity {
|
|||||||
|
|
||||||
private String extraReason;
|
private String extraReason;
|
||||||
|
|
||||||
|
private BigDecimal calcAmount;
|
||||||
|
|
||||||
|
private String calcDetail;
|
||||||
|
|
||||||
private BigDecimal totalAmount;
|
private BigDecimal totalAmount;
|
||||||
|
|
||||||
private String isMakeup;
|
private String isMakeup;
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ public class GearWageEntryDetailVo {
|
|||||||
@ExcelProperty(value = "额外原因")
|
@ExcelProperty(value = "额外原因")
|
||||||
private String extraReason;
|
private String extraReason;
|
||||||
|
|
||||||
|
@ExcelProperty(value = "明细计算金额")
|
||||||
|
private BigDecimal calcAmount;
|
||||||
|
|
||||||
|
private String calcDetail;
|
||||||
|
|
||||||
@ExcelProperty(value = "总金额")
|
@ExcelProperty(value = "总金额")
|
||||||
private BigDecimal totalAmount;
|
private BigDecimal totalAmount;
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ public class GearWageEntryDetailServiceImpl implements IGearWageEntryDetailServi
|
|||||||
private void fillAmountFields(GearWageEntryDetail entity) {
|
private void fillAmountFields(GearWageEntryDetail entity) {
|
||||||
BigDecimal workload = entity.getWorkload() == null ? BigDecimal.ZERO : entity.getWorkload();
|
BigDecimal workload = entity.getWorkload() == null ? BigDecimal.ZERO : entity.getWorkload();
|
||||||
BigDecimal extraAmount = entity.getExtraAmount() == null ? BigDecimal.ZERO : entity.getExtraAmount();
|
BigDecimal extraAmount = entity.getExtraAmount() == null ? BigDecimal.ZERO : entity.getExtraAmount();
|
||||||
|
BigDecimal calcAmount = entity.getCalcAmount() == null ? BigDecimal.ZERO : entity.getCalcAmount();
|
||||||
|
|
||||||
// 小时工/天工默认按1个计量单位计算基础金额(1小时或1天)
|
// 小时工/天工默认按1个计量单位计算基础金额(1小时或1天)
|
||||||
if (("1".equals(entity.getBillingType()) || "3".equals(entity.getBillingType())) && workload.compareTo(BigDecimal.ZERO) <= 0) {
|
if (("1".equals(entity.getBillingType()) || "3".equals(entity.getBillingType())) && workload.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
@@ -212,7 +213,8 @@ public class GearWageEntryDetailServiceImpl implements IGearWageEntryDetailServi
|
|||||||
BigDecimal baseAmount = workload.multiply(unitPrice);
|
BigDecimal baseAmount = workload.multiply(unitPrice);
|
||||||
entity.setBaseAmount(baseAmount);
|
entity.setBaseAmount(baseAmount);
|
||||||
entity.setExtraAmount(extraAmount);
|
entity.setExtraAmount(extraAmount);
|
||||||
entity.setTotalAmount(baseAmount.add(extraAmount));
|
entity.setCalcAmount(calcAmount);
|
||||||
|
entity.setTotalAmount(baseAmount.add(extraAmount).add(calcAmount));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fillMakeupFields(GearWageEntryDetail entity) {
|
private void fillMakeupFields(GearWageEntryDetail entity) {
|
||||||
|
|||||||
@@ -34,3 +34,11 @@ export function delProductAddition(addId) {
|
|||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function batchSaveProductAddition(data) {
|
||||||
|
return request({
|
||||||
|
url: '/api/mat/productAddition/batchSave',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,3 +31,10 @@ export function delProductLabor(laborId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function batchSaveProductLabor(data) {
|
||||||
|
return request({
|
||||||
|
url: '/api/mat/productLabor/batchSave',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,167 +1,165 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container product-detail">
|
||||||
<el-card class="mb20">
|
<el-card class="detail-card" v-loading="loading">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>{{ productDetail.productName }} - 产品详情</span>
|
<div class="header-title">
|
||||||
<el-button type="primary" plain size="small" @click="handleBack">返回列表</el-button>
|
<div class="product-name">{{ productDetail.productName || '-' }}</div>
|
||||||
|
<div class="sub-title">产品详情</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" plain @click="handleBack">返回列表</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="product-info">
|
|
||||||
<div class="info-item">
|
<el-row :gutter="16">
|
||||||
<span class="label">产品名称:</span>
|
<el-col :xs="24" :md="14">
|
||||||
<span class="value">{{ productDetail.productName }}</span>
|
<el-descriptions class="detail-desc" :column="2" border>
|
||||||
|
<el-descriptions-item label="产品名称">
|
||||||
|
{{ productDetail.productName || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="产品单价">
|
||||||
|
{{ formatDecimal(productDetail.unitPrice) }} 元
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="产品规格">
|
||||||
|
{{ productDetail.spec || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="产品型号">
|
||||||
|
{{ productDetail.model || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">
|
||||||
|
<span class="remark-text">{{ productDetail.remark || '无' }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div class="section" v-loading="additionLoading">
|
||||||
|
<div class="section-title">附加属性</div>
|
||||||
|
<el-empty v-if="!productAdditionList.length" description="暂无附加属性" />
|
||||||
|
<el-descriptions v-else :column="2" border size="small">
|
||||||
|
<el-descriptions-item
|
||||||
|
v-for="item in productAdditionList"
|
||||||
|
:key="item.addId || item.attrName"
|
||||||
|
:label="item.attrName"
|
||||||
|
>
|
||||||
|
{{ item.attrValue || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="24" :md="10">
|
||||||
|
<el-tabs class="media-tabs" type="border-card">
|
||||||
|
<el-tab-pane label="图片">
|
||||||
|
<el-empty
|
||||||
|
v-if="!(productDetail.productImages && productDetail.productImages.trim())"
|
||||||
|
description="暂无图片"
|
||||||
|
/>
|
||||||
|
<div v-else class="image-grid">
|
||||||
|
<div
|
||||||
|
v-for="(image, index) in productDetail.productImages.split(',').filter(img => img.trim())"
|
||||||
|
:key="index"
|
||||||
|
class="image-item"
|
||||||
|
>
|
||||||
|
<el-image
|
||||||
|
:src="image"
|
||||||
|
:preview-src-list="productDetail.productImages.split(',').filter(img => img.trim())"
|
||||||
|
fit="cover"
|
||||||
|
class="image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="说明书">
|
||||||
|
<el-empty v-if="!pdfDisplayList.length" description="暂无说明书" />
|
||||||
|
<div v-else class="pdf-list">
|
||||||
|
<div v-for="(pdf, index) in pdfDisplayList" :key="index" class="pdf-item">
|
||||||
|
<div class="pdf-left">
|
||||||
|
<el-icon class="pdf-icon"><Document /></el-icon>
|
||||||
|
<div class="pdf-meta">
|
||||||
|
<div class="pdf-name" :title="pdf.name">{{ pdf.name }}</div>
|
||||||
|
<div class="pdf-sub">{{ pdf.ossId ? `ID: ${pdf.ossId}` : '' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pdf-actions">
|
||||||
|
<el-button type="primary" link @click="previewPdf(pdf)">预览</el-button>
|
||||||
|
<el-button type="success" link @click="downloadPdf(pdf)">下载</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="detail-card" v-loading="materialLoading">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="header-title">
|
||||||
|
<div class="product-name">成本明细</div>
|
||||||
|
<div class="sub-title">主材 / 辅材 / 工价</div>
|
||||||
|
</div>
|
||||||
|
<div class="total-badge">
|
||||||
|
合计:{{ formatDecimal(totalAmount) }} 元
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
</template>
|
||||||
<span class="label">产品规格:</span>
|
|
||||||
<span class="value">{{ productDetail.spec }}</span>
|
<div class="cost-section">
|
||||||
</div>
|
<div class="cost-title">主材</div>
|
||||||
<div class="info-item">
|
<el-empty v-if="!mainMaterials.length" description="暂无主材" />
|
||||||
<span class="label">产品型号:</span>
|
<div v-else>
|
||||||
<span class="value">{{ productDetail.model }}</span>
|
<el-table :data="mainMaterials" border stripe>
|
||||||
</div>
|
<el-table-column prop="materialName" label="配料名称" min-width="140" />
|
||||||
<div class="info-item">
|
<el-table-column prop="spec" label="材料规格" min-width="140" />
|
||||||
<span class="label">产品单价:</span>
|
<el-table-column prop="quantity" label="数量" width="120" align="center" />
|
||||||
<span class="value">{{ formatDecimal(productDetail.unitPrice) }} 元</span>
|
<el-table-column prop="price" label="单价" width="120" align="center">
|
||||||
</div>
|
<template #default="scope">{{ formatDecimal(scope.row.price) }}</template>
|
||||||
<div class="info-item">
|
</el-table-column>
|
||||||
<span class="label">备注:</span>
|
<el-table-column prop="subtotal" label="小计" width="120" align="center">
|
||||||
<span class="value">{{ productDetail.remark || '无' }}</span>
|
<template #default="scope">{{ formatDecimal(scope.row.subtotal) }}</template>
|
||||||
</div>
|
</el-table-column>
|
||||||
</div>
|
</el-table>
|
||||||
<!-- 产品附加属性 -->
|
<div class="section-summary">
|
||||||
<div class="product-addition" v-if="productAdditionList.length > 0">
|
主材小计:{{ formatDecimal(mainMaterials.reduce((sum, item) => sum + item.subtotal, 0)) }} 元
|
||||||
<h4>产品附加属性</h4>
|
|
||||||
<el-table :data="productAdditionList" style="width: 100%" border>
|
|
||||||
<el-table-column prop="attrName" label="属性名" width="150" />
|
|
||||||
<el-table-column prop="attrValue" label="属性值" />
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
<div class="product-images" v-if="productDetail.productImages && productDetail.productImages.trim()">
|
|
||||||
<h4>产品图片</h4>
|
|
||||||
<div class="image-list">
|
|
||||||
<el-image
|
|
||||||
v-for="(image, index) in productDetail.productImages.split(',').filter(img => img.trim())"
|
|
||||||
:key="index"
|
|
||||||
:src="image"
|
|
||||||
:preview-src-list="productDetail.productImages.split(',').filter(img => img.trim())"
|
|
||||||
style="width: 100px; height: 100px; margin-right: 10px;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="product-pdfs" v-if="pdfDisplayList.length > 0">
|
|
||||||
<h4>产品说明书</h4>
|
|
||||||
<div class="pdf-list">
|
|
||||||
<div
|
|
||||||
v-for="(pdf, index) in pdfDisplayList"
|
|
||||||
:key="index"
|
|
||||||
class="pdf-item"
|
|
||||||
>
|
|
||||||
<el-icon class="pdf-icon"><Document /></el-icon>
|
|
||||||
<span class="pdf-name" :title="pdf.name">{{ pdf.name }}</span>
|
|
||||||
<el-button type="primary" link size="small" @click="previewPdf(pdf)">预览</el-button>
|
|
||||||
<el-button type="success" link size="small" @click="downloadPdf(pdf)">下载</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card>
|
<div class="cost-section">
|
||||||
<template #header>
|
<div class="cost-title">辅材</div>
|
||||||
<div class="card-header">
|
<el-empty v-if="!auxiliaryMaterials.length" description="暂无辅材" />
|
||||||
<span>材料明细</span>
|
<div v-else>
|
||||||
</div>
|
<el-table :data="auxiliaryMaterials" border stripe>
|
||||||
</template>
|
<el-table-column prop="materialName" label="配料名称" min-width="140" />
|
||||||
|
<el-table-column prop="spec" label="材料规格" min-width="140" />
|
||||||
<!-- 主材部分 -->
|
<el-table-column prop="quantity" label="数量" width="120" align="center" />
|
||||||
<div class="material-section">
|
<el-table-column prop="price" label="单价" width="120" align="center">
|
||||||
<h3 class="section-title">主材</h3>
|
<template #default="scope">{{ formatDecimal(scope.row.price) }}</template>
|
||||||
<el-table :data="mainMaterials" style="width: 100%" border>
|
</el-table-column>
|
||||||
<el-table-column prop="materialName" label="配料名称" width="150" />
|
<el-table-column prop="subtotal" label="小计" width="120" align="center">
|
||||||
<el-table-column prop="spec" label="材料规格" width="150" />
|
<template #default="scope">{{ formatDecimal(scope.row.subtotal) }}</template>
|
||||||
<el-table-column prop="quantity" label="数量" width="100" align="center" />
|
</el-table-column>
|
||||||
<el-table-column prop="price" label="价格" width="100" align="center">
|
</el-table>
|
||||||
<template #default="scope">
|
<div class="section-summary">
|
||||||
{{ formatDecimal(scope.row.price) }}
|
辅材小计:{{ formatDecimal(auxiliaryMaterials.reduce((sum, item) => sum + item.subtotal, 0)) }} 元
|
||||||
</template>
|
</div>
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="subtotal" label="小计" width="100" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
{{ formatDecimal(scope.row.subtotal) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="section-summary" v-if="mainMaterials.length > 0">
|
|
||||||
<span>主材小计:{{ formatDecimal(mainMaterials.reduce((sum, item) => sum + item.subtotal, 0)) }} 元</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 辅材部分 -->
|
<div class="cost-section">
|
||||||
<div class="material-section">
|
<div class="cost-title">工价</div>
|
||||||
<h3 class="section-title">辅材</h3>
|
<el-empty v-if="!productLaborList.length" description="暂无工价" />
|
||||||
<el-table :data="auxiliaryMaterials" style="width: 100%" border>
|
<div v-else>
|
||||||
<el-table-column prop="materialName" label="配料名称" width="150" />
|
<el-table :data="productLaborList" border stripe>
|
||||||
<el-table-column prop="spec" label="材料规格" width="150" />
|
<el-table-column prop="laborName" label="工价说明" min-width="200" />
|
||||||
<el-table-column prop="quantity" label="数量" width="100" align="center" />
|
<el-table-column prop="laborPrice" label="金额(元)" width="160" align="center">
|
||||||
<el-table-column prop="price" label="价格" width="100" align="center">
|
<template #default="scope">{{ formatDecimal(scope.row.laborPrice) }}</template>
|
||||||
<template #default="scope">
|
</el-table-column>
|
||||||
{{ formatDecimal(scope.row.price) }}
|
</el-table>
|
||||||
</template>
|
<div class="section-summary">
|
||||||
</el-table-column>
|
工价小计:{{ formatDecimal(productLaborList.reduce((sum, item) => sum + (Number(item.laborPrice) || 0), 0)) }} 元
|
||||||
<el-table-column prop="subtotal" label="小计" width="100" align="center">
|
</div>
|
||||||
<template #default="scope">
|
|
||||||
{{ formatDecimal(scope.row.subtotal) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="section-summary" v-if="auxiliaryMaterials.length > 0">
|
|
||||||
<span>辅材小计:{{ formatDecimal(auxiliaryMaterials.reduce((sum, item) => sum + item.subtotal, 0)) }} 元</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 工价部分 -->
|
|
||||||
<div class="material-section">
|
|
||||||
<h3 class="section-title">工价</h3>
|
|
||||||
<el-table :data="laborMaterials" style="width: 100%" border>
|
|
||||||
<el-table-column prop="materialName" label="项目名称" width="150" />
|
|
||||||
<el-table-column prop="spec" label="规格" width="150" />
|
|
||||||
<el-table-column prop="quantity" label="数量" width="100" align="center" />
|
|
||||||
<el-table-column prop="price" label="价格" width="100" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
{{ formatDecimal(scope.row.price) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="subtotal" label="小计" width="100" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
{{ formatDecimal(scope.row.subtotal) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="section-summary" v-if="laborMaterials.length > 0">
|
|
||||||
<span>工价小计:{{ formatDecimal(laborMaterials.reduce((sum, item) => sum + item.subtotal, 0)) }} 元</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="material-section" v-if="productLaborList.length > 0">
|
|
||||||
<h3 class="section-title">工价(手动)</h3>
|
|
||||||
<el-table :data="productLaborList" style="width: 100%" border>
|
|
||||||
<el-table-column prop="laborName" label="工价说明" />
|
|
||||||
<el-table-column prop="laborPrice" label="金额" width="140" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
{{ formatDecimal(scope.row.laborPrice) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="section-summary">
|
|
||||||
<span>工价(手动)小计:{{ formatDecimal(productLaborList.reduce((sum, item) => sum + (Number(item.laborPrice) || 0), 0)) }} 元</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 总计部分 -->
|
|
||||||
<div class="total-section">
|
|
||||||
<div class="total-item">
|
|
||||||
<span class="total-label">合计:</span>
|
|
||||||
<span class="total-value">{{ formatDecimal(totalAmount) }} 元</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -224,29 +222,15 @@ const auxiliaryMaterials = computed(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
const laborMaterials = computed(() => {
|
|
||||||
// 工价(材料类型为3)
|
|
||||||
return productMaterialRelationList.value
|
|
||||||
.filter(item => item.material && item.material.materialType === 3)
|
|
||||||
.map(item => ({
|
|
||||||
materialName: item.material.materialName,
|
|
||||||
spec: item.material.spec,
|
|
||||||
quantity: item.materialNum + (item.material.unit || ''),
|
|
||||||
price: item.material.unitPrice || 0,
|
|
||||||
subtotal: (item.materialNum * (item.material.unitPrice || 0)) || 0
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算总金额
|
// 计算总金额
|
||||||
const totalAmount = computed(() => {
|
const totalAmount = computed(() => {
|
||||||
const mainTotal = mainMaterials.value.reduce((sum, item) => sum + item.subtotal, 0);
|
const mainTotal = mainMaterials.value.reduce((sum, item) => sum + item.subtotal, 0);
|
||||||
const auxiliaryTotal = auxiliaryMaterials.value.reduce((sum, item) => sum + item.subtotal, 0);
|
const auxiliaryTotal = auxiliaryMaterials.value.reduce((sum, item) => sum + item.subtotal, 0);
|
||||||
const laborTotal = laborMaterials.value.reduce((sum, item) => sum + item.subtotal, 0);
|
|
||||||
const manualLaborTotal = productLaborList.value.reduce((sum, item) => {
|
const manualLaborTotal = productLaborList.value.reduce((sum, item) => {
|
||||||
const price = item && item.laborPrice !== undefined && item.laborPrice !== null ? Number(item.laborPrice) : 0;
|
const price = item && item.laborPrice !== undefined && item.laborPrice !== null ? Number(item.laborPrice) : 0;
|
||||||
return sum + (Number.isFinite(price) ? price : 0);
|
return sum + (Number.isFinite(price) ? price : 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
return mainTotal + auxiliaryTotal + laborTotal + manualLaborTotal;
|
return mainTotal + auxiliaryTotal + manualLaborTotal;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isOssIdList = (val) => {
|
const isOssIdList = (val) => {
|
||||||
@@ -399,8 +383,8 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.app-container {
|
.detail-card {
|
||||||
padding: 20px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
@@ -409,51 +393,87 @@ onMounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-info {
|
.header-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 4px;
|
||||||
margin: 20px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item {
|
.product-name {
|
||||||
display: flex;
|
font-weight: 600;
|
||||||
align-items: center;
|
font-size: 16px;
|
||||||
gap: 10px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.sub-title {
|
||||||
font-weight: bold;
|
color: var(--el-text-color-secondary);
|
||||||
min-width: 80px;
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-images {
|
.detail-desc :deep(.el-descriptions__label) {
|
||||||
margin-top: 20px;
|
width: 88px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-list {
|
.remark-text {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-tabs :deep(.el-tabs__content) {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-pdfs {
|
.image-item {
|
||||||
margin-top: 20px;
|
width: 112px;
|
||||||
|
height: 112px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
background: var(--el-fill-color-blank);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-list {
|
.pdf-list {
|
||||||
margin-top: 10px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-item {
|
.pdf-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--el-fill-color-blank);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 10px;
|
min-width: 0;
|
||||||
background-color: #f5f7fa;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-icon {
|
.pdf-icon {
|
||||||
@@ -461,56 +481,49 @@ onMounted(() => {
|
|||||||
color: #409eff;
|
color: #409eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdf-meta {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.pdf-name {
|
.pdf-name {
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
max-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-section {
|
.pdf-sub {
|
||||||
margin: 20px 0;
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.pdf-actions {
|
||||||
background-color: #f5f7fa;
|
display: flex;
|
||||||
padding: 10px;
|
align-items: center;
|
||||||
border-left: 4px solid #409eff;
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-badge {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-section {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-title {
|
||||||
|
font-weight: 600;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-summary {
|
.section-summary {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #f9f9f9;
|
background-color: var(--el-fill-color-light);
|
||||||
border-top: 1px solid #e4e7ed;
|
border-top: 1px solid var(--el-border-color-light);
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-section {
|
|
||||||
margin-top: 30px;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #f0f9eb;
|
|
||||||
border: 1px solid #b7eb8f;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.total-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.total-label {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.total-value {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #f56c6c;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -61,14 +61,16 @@
|
|||||||
<span v-else>无</span>
|
<span v-else>无</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="320" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button link type="primary" icon="Plus" @click="handleBom(scope.row)">配方</el-button>
|
<div class="product-op-actions">
|
||||||
<el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
|
<el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
|
||||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
|
<el-button link type="primary" icon="Plus" @click="handleBom(scope.row)">配方</el-button>
|
||||||
<el-button link type="primary" icon="Setting" @click="handleAddition(scope.row)">附加属性</el-button>
|
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
|
||||||
<el-button link type="primary" @click="handleLabor(scope.row)">工价</el-button>
|
<el-button link type="primary" icon="Setting" @click="handleAddition(scope.row)">附加属性</el-button>
|
||||||
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
|
<el-button link type="primary" icon="Money" @click="handleLabor(scope.row)">工价</el-button>
|
||||||
|
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -238,8 +240,8 @@ import { useRouter } from 'vue-router';
|
|||||||
import { listProduct, getProduct, delProduct, addProduct, updateProduct } from "@/api/mat/product";
|
import { listProduct, getProduct, delProduct, addProduct, updateProduct } from "@/api/mat/product";
|
||||||
import { listProductMaterialRelation } from "@/api/mat/productMaterialRelation";
|
import { listProductMaterialRelation } from "@/api/mat/productMaterialRelation";
|
||||||
import { getMaterial } from "@/api/mat/material";
|
import { getMaterial } from "@/api/mat/material";
|
||||||
import { listProductAddition, addProductAddition, updateProductAddition, delProductAddition } from "@/api/mat/productAddition";
|
import { listProductAddition, batchSaveProductAddition } from "@/api/mat/productAddition";
|
||||||
import { listProductLabor, addProductLabor, updateProductLabor, delProductLabor } from "@/api/mat/productLabor";
|
import { listProductLabor, batchSaveProductLabor } from "@/api/mat/productLabor";
|
||||||
import { listByIds, listOss } from "@/api/system/oss";
|
import { listByIds, listOss } from "@/api/system/oss";
|
||||||
import bom from "@/views/mat/components/bom.vue";
|
import bom from "@/views/mat/components/bom.vue";
|
||||||
import StickyDragContainer from "@/components/StickyDragContainer/index.vue";
|
import StickyDragContainer from "@/components/StickyDragContainer/index.vue";
|
||||||
@@ -672,41 +674,27 @@ function removeAdditionItem(index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveAdditions() {
|
function saveAdditions() {
|
||||||
// 过滤掉空的属性项
|
const items = (additionList.value || [])
|
||||||
const validAdditions = additionList.value.filter(item => item.attrName && item.attrName.trim());
|
.map(item => ({
|
||||||
|
addId: item?.addId,
|
||||||
// 保存附加属性
|
attrName: item?.attrName ? String(item.attrName).trim() : '',
|
||||||
validAdditions.forEach(item => {
|
attrValue: item?.attrValue ? String(item.attrValue).trim() : ''
|
||||||
const additionData = {
|
}))
|
||||||
productId: currentProductId.value,
|
.filter(item => item.attrName);
|
||||||
attrName: item.attrName.trim(),
|
|
||||||
attrValue: item.attrValue ? item.attrValue.trim() : ''
|
batchSaveProductAddition({
|
||||||
};
|
productId: currentProductId.value,
|
||||||
|
items
|
||||||
// 如果有addId,则是更新操作
|
}).then(res => {
|
||||||
if (item.addId) {
|
if (res.code === 200 && res.data) {
|
||||||
additionData.addId = item.addId;
|
proxy.$modal.msgSuccess('保存成功');
|
||||||
// 调用API更新附加属性
|
additionOpen.value = false;
|
||||||
updateProductAddition(additionData).then(response => {
|
|
||||||
if (response.code === 200) {
|
|
||||||
proxy.$modal.msgSuccess('保存成功');
|
|
||||||
} else {
|
|
||||||
proxy.$modal.msgError('保存失败');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// 调用API新增附加属性
|
proxy.$modal.msgError('保存失败');
|
||||||
addProductAddition(additionData).then(response => {
|
|
||||||
if (response.code === 200) {
|
|
||||||
proxy.$modal.msgSuccess('保存成功');
|
|
||||||
} else {
|
|
||||||
proxy.$modal.msgError('保存失败');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
proxy.$modal.msgError('保存失败');
|
||||||
});
|
});
|
||||||
|
|
||||||
additionOpen.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLabor(row) {
|
function handleLabor(row) {
|
||||||
@@ -725,38 +713,28 @@ function addLaborItem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeLaborItem(index) {
|
function removeLaborItem(index) {
|
||||||
const item = laborList.value[index];
|
|
||||||
if (item && item.laborId) {
|
|
||||||
delProductLabor(item.laborId).finally(() => {
|
|
||||||
laborList.value.splice(index, 1);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
laborList.value.splice(index, 1);
|
laborList.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveLabors() {
|
async function saveLabors() {
|
||||||
const valid = laborList.value
|
|
||||||
.map(item => ({
|
|
||||||
...item,
|
|
||||||
laborName: item.laborName ? String(item.laborName).trim() : ''
|
|
||||||
}))
|
|
||||||
.filter(item => item.laborName);
|
|
||||||
|
|
||||||
const tasks = valid.map(item => {
|
|
||||||
const data = {
|
|
||||||
laborId: item.laborId,
|
|
||||||
productId: currentProductId.value,
|
|
||||||
laborName: item.laborName,
|
|
||||||
laborPrice: item.laborPrice ?? 0
|
|
||||||
};
|
|
||||||
if (data.laborId) return updateProductLabor(data);
|
|
||||||
return addProductLabor(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(tasks);
|
const items = (laborList.value || [])
|
||||||
proxy.$modal.msgSuccess('保存成功');
|
.map(item => ({
|
||||||
|
laborId: item?.laborId,
|
||||||
|
laborName: item?.laborName ? String(item.laborName).trim() : '',
|
||||||
|
laborPrice: item?.laborPrice ?? 0
|
||||||
|
}))
|
||||||
|
.filter(item => item.laborName);
|
||||||
|
|
||||||
|
const res = await batchSaveProductLabor({
|
||||||
|
productId: currentProductId.value,
|
||||||
|
items
|
||||||
|
});
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
proxy.$modal.msgSuccess('保存成功');
|
||||||
|
} else {
|
||||||
|
proxy.$modal.msgError('保存失败');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
proxy.$modal.msgError('保存失败');
|
proxy.$modal.msgError('保存失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -787,4 +765,15 @@ getList();
|
|||||||
:deep(.el-image-viewer__wrapper) {
|
:deep(.el-image-viewer__wrapper) {
|
||||||
z-index: 9999 !important;
|
z-index: 9999 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-op-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.product-op-actions .el-button + .el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -74,17 +74,54 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="明细金额" align="center" prop="calcAmount" width="110">
|
||||||
|
<template #default="scope">{{ round2(toNumber(scope.row.calcAmount)) }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="总金额" align="center" prop="totalAmount" width="110" />
|
<el-table-column label="总金额" align="center" prop="totalAmount" width="110" />
|
||||||
<el-table-column label="累计金额" align="center" prop="cumulativeAmount" width="110" />
|
<el-table-column label="累计金额" align="center" prop="cumulativeAmount" width="110" />
|
||||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="170">
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button link type="primary" icon="Check" @click="saveRow(scope.row)">保存</el-button>
|
<el-button link type="primary" icon="Check" @click="saveRow(scope.row)">保存</el-button>
|
||||||
|
<el-button link type="primary" icon="Document" @click="openCalcDialog(scope.row)">明细</el-button>
|
||||||
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
|
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||||
|
|
||||||
|
<el-dialog v-model="calcOpen" title="明细计算" width="720px" append-to-body>
|
||||||
|
<div class="calc-toolbar">
|
||||||
|
<el-button size="small" type="primary" plain icon="Plus" @click="addCalcLine">新增一行</el-button>
|
||||||
|
<div class="calc-sum">合计:{{ calcTotal }} 元</div>
|
||||||
|
</div>
|
||||||
|
<el-table :data="calcLines" border stripe>
|
||||||
|
<el-table-column label="数值A" min-width="160">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-input v-model="scope.row.a" placeholder="请输入" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="数值B" min-width="160">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-input v-model="scope.row.b" placeholder="请输入" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="小计(A×B)" width="150" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ lineAmount(scope.row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button link type="danger" icon="Delete" @click="removeCalcLine(scope.$index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="calcOpen = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveCalcToRow">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -167,13 +204,83 @@ function round2(v) {
|
|||||||
return Math.round(v * 100) / 100
|
return Math.round(v * 100) / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const calcOpen = ref(false)
|
||||||
|
const calcTargetRow = ref(null)
|
||||||
|
const calcLines = ref([])
|
||||||
|
|
||||||
|
function safeParseCalcDetail(val) {
|
||||||
|
if (!val) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(val)
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCalcDialog(row) {
|
||||||
|
calcTargetRow.value = row
|
||||||
|
const parsed = safeParseCalcDetail(row.calcDetail)
|
||||||
|
const items = parsed && Array.isArray(parsed.items) ? parsed.items : null
|
||||||
|
if (items && items.length) {
|
||||||
|
calcLines.value = items.map(i => ({
|
||||||
|
a: i && i.a !== undefined && i.a !== null ? String(i.a) : '',
|
||||||
|
b: i && i.b !== undefined && i.b !== null ? String(i.b) : ''
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
calcLines.value = [{ a: '', b: '' }]
|
||||||
|
}
|
||||||
|
calcOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCalcLine() {
|
||||||
|
calcLines.value.push({ a: '', b: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCalcLine(index) {
|
||||||
|
if (calcLines.value.length <= 1) {
|
||||||
|
calcLines.value = [{ a: '', b: '' }]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
calcLines.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineAmount(line) {
|
||||||
|
return round2(toNumber(line?.a) * toNumber(line?.b))
|
||||||
|
}
|
||||||
|
|
||||||
|
const calcTotal = computed(() => {
|
||||||
|
const sum = calcLines.value.reduce((acc, line) => acc + lineAmount(line), 0)
|
||||||
|
return round2(sum)
|
||||||
|
})
|
||||||
|
|
||||||
|
function saveCalcToRow() {
|
||||||
|
const row = calcTargetRow.value
|
||||||
|
if (!row) {
|
||||||
|
calcOpen.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const normalizedItems = calcLines.value
|
||||||
|
.map(l => ({
|
||||||
|
a: l && l.a !== undefined && l.a !== null ? String(l.a).trim() : '',
|
||||||
|
b: l && l.b !== undefined && l.b !== null ? String(l.b).trim() : ''
|
||||||
|
}))
|
||||||
|
.filter(l => l.a !== '' || l.b !== '')
|
||||||
|
|
||||||
|
row.calcAmount = calcTotal.value
|
||||||
|
row.calcDetail = JSON.stringify({ v: 1, items: normalizedItems })
|
||||||
|
recalcRowAmount(row)
|
||||||
|
calcOpen.value = false
|
||||||
|
proxy.$modal.msgSuccess('明细已保存,请点击该行“保存”提交')
|
||||||
|
}
|
||||||
|
|
||||||
function recalcRowAmount(row) {
|
function recalcRowAmount(row) {
|
||||||
const workload = toNumber(row.workload)
|
const workload = toNumber(row.workload)
|
||||||
const unitPrice = row.billingType === '2' ? toNumber(row.unitPrice) : toNumber(row.unitPrice)
|
const unitPrice = row.billingType === '2' ? toNumber(row.unitPrice) : toNumber(row.unitPrice)
|
||||||
const extraAmount = toNumber(row.extraAmount)
|
const extraAmount = toNumber(row.extraAmount)
|
||||||
|
const calcAmount = toNumber(row.calcAmount)
|
||||||
const baseAmount = round2(workload * unitPrice)
|
const baseAmount = round2(workload * unitPrice)
|
||||||
row.baseAmount = baseAmount
|
row.baseAmount = baseAmount
|
||||||
row.totalAmount = round2(baseAmount + extraAmount)
|
row.totalAmount = round2(baseAmount + extraAmount + calcAmount)
|
||||||
|
|
||||||
// 重新计算累计金额
|
// 重新计算累计金额
|
||||||
updateCumulativeAmounts()
|
updateCumulativeAmounts()
|
||||||
@@ -210,6 +317,8 @@ function getList() {
|
|||||||
workload: normalizeEditableValue(row.workload),
|
workload: normalizeEditableValue(row.workload),
|
||||||
unitPrice: normalizeEditableValue(row.unitPrice),
|
unitPrice: normalizeEditableValue(row.unitPrice),
|
||||||
extraAmount: normalizeEditableValue(row.extraAmount),
|
extraAmount: normalizeEditableValue(row.extraAmount),
|
||||||
|
calcAmount: row.calcAmount ?? 0,
|
||||||
|
calcDetail: row.calcDetail,
|
||||||
cumulativeAmount: cumulativeAmounts.value[row.empName] || 0
|
cumulativeAmount: cumulativeAmounts.value[row.empName] || 0
|
||||||
}
|
}
|
||||||
recalcRowAmount(r)
|
recalcRowAmount(r)
|
||||||
@@ -241,6 +350,9 @@ function buildRowPayload(row) {
|
|||||||
if (payload.extraAmount === '' || payload.extraAmount === null || payload.extraAmount === undefined) {
|
if (payload.extraAmount === '' || payload.extraAmount === null || payload.extraAmount === undefined) {
|
||||||
payload.extraAmount = 0
|
payload.extraAmount = 0
|
||||||
}
|
}
|
||||||
|
if (payload.calcAmount === '' || payload.calcAmount === null || payload.calcAmount === undefined) {
|
||||||
|
payload.calcAmount = 0
|
||||||
|
}
|
||||||
if (payload.billingType !== '2') {
|
if (payload.billingType !== '2') {
|
||||||
payload.unitPrice = null
|
payload.unitPrice = null
|
||||||
} else if (payload.unitPrice === '' || payload.unitPrice === null || payload.unitPrice === undefined) {
|
} else if (payload.unitPrice === '' || payload.unitPrice === null || payload.unitPrice === undefined) {
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ CREATE TABLE gear_wage_entry_detail (
|
|||||||
base_amount decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '基础金额(工作量*单价)',
|
base_amount decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '基础金额(工作量*单价)',
|
||||||
extra_amount decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '额外金额(高温/交通等)',
|
extra_amount decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '额外金额(高温/交通等)',
|
||||||
extra_reason varchar(255) DEFAULT '' COMMENT '额外金额原因',
|
extra_reason varchar(255) DEFAULT '' COMMENT '额外金额原因',
|
||||||
total_amount decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '总金额(基础+额外)',
|
calc_amount decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '明细计算金额(多行乘法合计)',
|
||||||
|
calc_detail text COMMENT '明细计算行JSON(两列乘法)',
|
||||||
|
total_amount decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '总金额(基础+额外+明细计算)',
|
||||||
is_makeup char(1) NOT NULL DEFAULT '0' COMMENT '是否补录(0否 1是)',
|
is_makeup char(1) NOT NULL DEFAULT '0' COMMENT '是否补录(0否 1是)',
|
||||||
source_detail_id bigint(20) DEFAULT NULL COMMENT '被补录/被修改的原始明细ID',
|
source_detail_id bigint(20) DEFAULT NULL COMMENT '被补录/被修改的原始明细ID',
|
||||||
makeup_responsible_id bigint(20) DEFAULT NULL COMMENT '补录责任人ID',
|
makeup_responsible_id bigint(20) DEFAULT NULL COMMENT '补录责任人ID',
|
||||||
|
|||||||
Reference in New Issue
Block a user