明细计算,产品详情页

This commit is contained in:
朱昊天
2026-04-28 16:53:35 +08:00
parent 539889a346
commit fe13e952f2
18 changed files with 633 additions and 298 deletions

View File

@@ -8,6 +8,7 @@ import com.gear.common.core.controller.BaseController;
import com.gear.mat.domain.bo.MatProductAdditionBo;
import com.gear.mat.domain.vo.MatProductAdditionVo;
import com.gear.mat.service.IMatProductAdditionService;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -69,4 +70,19 @@ public class MatProductAdditionController extends BaseController {
boolean result = productAdditionService.delProductAddition(addId);
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;
}
}

View File

@@ -8,6 +8,7 @@ import com.gear.common.enums.BusinessType;
import com.gear.mat.domain.bo.MatProductLaborBo;
import com.gear.mat.domain.vo.MatProductLaborVo;
import com.gear.mat.service.IMatProductLaborService;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@@ -46,4 +47,19 @@ public class MatProductLaborController extends BaseController {
boolean result = productLaborService.delProductLabor(laborId);
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;
}
}

View File

@@ -71,6 +71,11 @@ public class MatMaterialVo {
@ExcelDictFormat(readConverterExp = "已=入库")
private BigDecimal currentStock;
/**
* 单价(用于成本测算:默认取最新入库单价,其次取最新采购单价)
*/
private BigDecimal unitPrice;
/**
* 备注
*/

View File

@@ -44,4 +44,6 @@ public interface IMatProductAdditionService extends IService<MatProductAddition>
* @return 删除结果
*/
boolean delProductAddition(Long addId);
boolean batchSaveProductAddition(Long productId, List<MatProductAdditionBo> items);
}

View File

@@ -16,4 +16,6 @@ public interface IMatProductLaborService extends IService<MatProductLabor> {
boolean updateProductLabor(MatProductLaborBo productLaborBo);
boolean delProductLabor(Long laborId);
boolean batchSaveProductLabor(Long productId, List<MatProductLaborBo> items);
}

View File

@@ -49,7 +49,41 @@ public class MatMaterialServiceImpl implements IMatMaterialService {
*/
@Override
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;
}
/**

View File

@@ -9,7 +9,10 @@ import com.gear.mat.mapper.MatProductAdditionMapper;
import com.gear.mat.service.IMatProductAdditionService;
import com.gear.common.utils.BeanCopyUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 产品属性附加表Service实现类
@@ -51,4 +54,55 @@ public class MatProductAdditionServiceImpl extends ServiceImpl<MatProductAdditio
public boolean delProductAddition(Long 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;
}
}

View File

@@ -9,8 +9,11 @@ import com.gear.mat.domain.vo.MatProductLaborVo;
import com.gear.mat.mapper.MatProductLaborMapper;
import com.gear.mat.service.IMatProductLaborService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class MatProductLaborServiceImpl extends ServiceImpl<MatProductLaborMapper, MatProductLabor> implements IMatProductLaborService {
@@ -42,4 +45,55 @@ public class MatProductLaborServiceImpl extends ServiceImpl<MatProductLaborMappe
public boolean delProductLabor(Long 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;
}
}

View File

@@ -57,6 +57,16 @@ public class GearWageEntryDetail extends BaseEntity {
private String extraReason;
/**
* 明细计算金额(多行乘法合计)
*/
private BigDecimal calcAmount;
/**
* 明细计算行JSON两列乘法
*/
private String calcDetail;
private BigDecimal totalAmount;
/**

View File

@@ -59,6 +59,10 @@ public class GearWageEntryDetailBo extends BaseEntity {
private String extraReason;
private BigDecimal calcAmount;
private String calcDetail;
private BigDecimal totalAmount;
private String isMakeup;

View File

@@ -63,6 +63,11 @@ public class GearWageEntryDetailVo {
@ExcelProperty(value = "额外原因")
private String extraReason;
@ExcelProperty(value = "明细计算金额")
private BigDecimal calcAmount;
private String calcDetail;
@ExcelProperty(value = "总金额")
private BigDecimal totalAmount;

View File

@@ -188,6 +188,7 @@ public class GearWageEntryDetailServiceImpl implements IGearWageEntryDetailServi
private void fillAmountFields(GearWageEntryDetail entity) {
BigDecimal workload = entity.getWorkload() == null ? BigDecimal.ZERO : entity.getWorkload();
BigDecimal extraAmount = entity.getExtraAmount() == null ? BigDecimal.ZERO : entity.getExtraAmount();
BigDecimal calcAmount = entity.getCalcAmount() == null ? BigDecimal.ZERO : entity.getCalcAmount();
// 小时工/天工默认按1个计量单位计算基础金额1小时或1天
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);
entity.setBaseAmount(baseAmount);
entity.setExtraAmount(extraAmount);
entity.setTotalAmount(baseAmount.add(extraAmount));
entity.setCalcAmount(calcAmount);
entity.setTotalAmount(baseAmount.add(extraAmount).add(calcAmount));
}
private void fillMakeupFields(GearWageEntryDetail entity) {

View File

@@ -34,3 +34,11 @@ export function delProductAddition(addId) {
method: 'delete'
})
}
export function batchSaveProductAddition(data) {
return request({
url: '/api/mat/productAddition/batchSave',
method: 'post',
data
})
}

View File

@@ -31,3 +31,10 @@ export function delProductLabor(laborId) {
})
}
export function batchSaveProductLabor(data) {
return request({
url: '/api/mat/productLabor/batchSave',
method: 'post',
data
})
}

View File

@@ -1,167 +1,165 @@
<template>
<div class="app-container">
<el-card class="mb20">
<div class="app-container product-detail">
<el-card class="detail-card" v-loading="loading">
<template #header>
<div class="card-header">
<span>{{ productDetail.productName }} - 产品详情</span>
<el-button type="primary" plain size="small" @click="handleBack">返回列表</el-button>
<div class="header-title">
<div class="product-name">{{ productDetail.productName || '-' }}</div>
<div class="sub-title">产品详情</div>
</div>
<el-button type="primary" plain @click="handleBack">返回列表</el-button>
</div>
</template>
<div class="product-info">
<div class="info-item">
<span class="label">产品名称</span>
<span class="value">{{ productDetail.productName }}</span>
<el-row :gutter="16">
<el-col :xs="24" :md="14">
<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>
<div class="info-item">
<span class="label">产品规格</span>
<span class="value">{{ productDetail.spec }}</span>
</div>
<div class="info-item">
<span class="label">产品型号</span>
<span class="value">{{ productDetail.model }}</span>
</div>
<div class="info-item">
<span class="label">产品单价</span>
<span class="value">{{ formatDecimal(productDetail.unitPrice) }} </span>
</div>
<div class="info-item">
<span class="label">备注</span>
<span class="value">{{ productDetail.remark || '无' }}</span>
</div>
</div>
<!-- 产品附加属性 -->
<div class="product-addition" v-if="productAdditionList.length > 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
</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())"
style="width: 100px; height: 100px; margin-right: 10px;"
fit="cover"
class="image"
/>
</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-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>
<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 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>
<el-card class="detail-card" v-loading="materialLoading">
<template #header>
<div class="card-header">
<span>材料明细</span>
<div class="header-title">
<div class="product-name">成本明细</div>
<div class="sub-title">主材 / 辅材 / 工价</div>
</div>
<div class="total-badge">
合计:{{ formatDecimal(totalAmount) }} 元
</div>
</div>
</template>
<!-- 主材部分 -->
<div class="material-section">
<h3 class="section-title">主材</h3>
<el-table :data="mainMaterials" 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>
<div class="cost-section">
<div class="cost-title">主材</div>
<el-empty v-if="!mainMaterials.length" description="暂无主材" />
<div v-else>
<el-table :data="mainMaterials" border stripe>
<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" />
<el-table-column prop="price" label="单价" width="120" 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="mainMaterials.length > 0">
<span>主材小计:{{ formatDecimal(mainMaterials.reduce((sum, item) => sum + item.subtotal, 0)) }} 元</span>
</div>
</div>
<!-- 辅材部分 -->
<div class="material-section">
<h3 class="section-title">辅材</h3>
<el-table :data="auxiliaryMaterials" 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="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 prop="subtotal" label="小计" width="120" align="center">
<template #default="scope">{{ formatDecimal(scope.row.subtotal) }}</template>
</el-table-column>
</el-table>
<div class="section-summary">
<span>工价(手动)小计:{{ formatDecimal(productLaborList.reduce((sum, item) => sum + (Number(item.laborPrice) || 0), 0)) }} 元</span>
主材小计:{{ formatDecimal(mainMaterials.reduce((sum, item) => sum + item.subtotal, 0)) }} 元
</div>
</div>
</div>
<!-- 总计部分 -->
<div class="total-section">
<div class="total-item">
<span class="total-label">合计:</span>
<span class="total-value">{{ formatDecimal(totalAmount) }} 元</span>
<div class="cost-section">
<div class="cost-title">辅材</div>
<el-empty v-if="!auxiliaryMaterials.length" description="暂无辅材" />
<div v-else>
<el-table :data="auxiliaryMaterials" border stripe>
<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" />
<el-table-column prop="price" label="单价" width="120" align="center">
<template #default="scope">{{ formatDecimal(scope.row.price) }}</template>
</el-table-column>
<el-table-column prop="subtotal" label="小计" width="120" align="center">
<template #default="scope">{{ formatDecimal(scope.row.subtotal) }}</template>
</el-table-column>
</el-table>
<div class="section-summary">
辅材小计:{{ formatDecimal(auxiliaryMaterials.reduce((sum, item) => sum + item.subtotal, 0)) }} 元
</div>
</div>
</div>
<div class="cost-section">
<div class="cost-title">工价</div>
<el-empty v-if="!productLaborList.length" description="暂无工价" />
<div v-else>
<el-table :data="productLaborList" border stripe>
<el-table-column prop="laborName" label="工价说明" min-width="200" />
<el-table-column prop="laborPrice" label="金额()" width="160" align="center">
<template #default="scope">{{ formatDecimal(scope.row.laborPrice) }}</template>
</el-table-column>
</el-table>
<div class="section-summary">
工价小计:{{ formatDecimal(productLaborList.reduce((sum, item) => sum + (Number(item.laborPrice) || 0), 0)) }} 元
</div>
</div>
</div>
</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 mainTotal = mainMaterials.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 price = item && item.laborPrice !== undefined && item.laborPrice !== null ? Number(item.laborPrice) : 0;
return sum + (Number.isFinite(price) ? price : 0);
}, 0);
return mainTotal + auxiliaryTotal + laborTotal + manualLaborTotal;
return mainTotal + auxiliaryTotal + manualLaborTotal;
});
const isOssIdList = (val) => {
@@ -399,8 +383,8 @@ onMounted(() => {
</script>
<style scoped>
.app-container {
padding: 20px;
.detail-card {
margin-bottom: 16px;
}
.card-header {
@@ -409,51 +393,87 @@ onMounted(() => {
align-items: center;
}
.product-info {
.header-title {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 20px 0;
flex-direction: column;
gap: 4px;
}
.info-item {
display: flex;
align-items: center;
gap: 10px;
.product-name {
font-weight: 600;
font-size: 16px;
line-height: 22px;
}
.label {
font-weight: bold;
min-width: 80px;
.sub-title {
color: var(--el-text-color-secondary);
font-size: 12px;
line-height: 18px;
}
.product-images {
margin-top: 20px;
.detail-desc :deep(.el-descriptions__label) {
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;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.product-pdfs {
margin-top: 20px;
.image-item {
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 {
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
.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;
align-items: center;
gap: 10px;
padding: 10px;
background-color: #f5f7fa;
border-radius: 4px;
margin-bottom: 8px;
min-width: 0;
}
.pdf-icon {
@@ -461,56 +481,49 @@ onMounted(() => {
color: #409eff;
}
.pdf-meta {
min-width: 0;
}
.pdf-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 320px;
}
.material-section {
margin: 20px 0;
.pdf-sub {
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 18px;
}
.section-title {
background-color: #f5f7fa;
padding: 10px;
border-left: 4px solid #409eff;
.pdf-actions {
display: flex;
align-items: center;
gap: 6px;
}
.total-badge {
font-weight: 600;
color: #f56c6c;
}
.cost-section {
margin-top: 14px;
}
.cost-title {
font-weight: 600;
margin-bottom: 10px;
}
.section-summary {
text-align: right;
padding: 10px;
background-color: #f9f9f9;
border-top: 1px solid #e4e7ed;
background-color: var(--el-fill-color-light);
border-top: 1px solid var(--el-border-color-light);
margin-top: -1px;
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>

View File

@@ -61,14 +61,16 @@
<span v-else></span>
</template>
</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">
<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="Plus" @click="handleBom(scope.row)">配方</el-button>
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button link type="primary" icon="Setting" @click="handleAddition(scope.row)">附加属性</el-button>
<el-button link type="primary" @click="handleLabor(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>
</el-table-column>
</el-table>
@@ -238,8 +240,8 @@ import { useRouter } from 'vue-router';
import { listProduct, getProduct, delProduct, addProduct, updateProduct } from "@/api/mat/product";
import { listProductMaterialRelation } from "@/api/mat/productMaterialRelation";
import { getMaterial } from "@/api/mat/material";
import { listProductAddition, addProductAddition, updateProductAddition, delProductAddition } from "@/api/mat/productAddition";
import { listProductLabor, addProductLabor, updateProductLabor, delProductLabor } from "@/api/mat/productLabor";
import { listProductAddition, batchSaveProductAddition } from "@/api/mat/productAddition";
import { listProductLabor, batchSaveProductLabor } from "@/api/mat/productLabor";
import { listByIds, listOss } from "@/api/system/oss";
import bom from "@/views/mat/components/bom.vue";
import StickyDragContainer from "@/components/StickyDragContainer/index.vue";
@@ -672,41 +674,27 @@ function removeAdditionItem(index) {
}
function saveAdditions() {
// 过滤掉空的属性项
const validAdditions = additionList.value.filter(item => item.attrName && item.attrName.trim());
const items = (additionList.value || [])
.map(item => ({
addId: item?.addId,
attrName: item?.attrName ? String(item.attrName).trim() : '',
attrValue: item?.attrValue ? String(item.attrValue).trim() : ''
}))
.filter(item => item.attrName);
// 保存附加属性
validAdditions.forEach(item => {
const additionData = {
batchSaveProductAddition({
productId: currentProductId.value,
attrName: item.attrName.trim(),
attrValue: item.attrValue ? item.attrValue.trim() : ''
};
// 如果有addId则是更新操作
if (item.addId) {
additionData.addId = item.addId;
// 调用API更新附加属性
updateProductAddition(additionData).then(response => {
if (response.code === 200) {
items
}).then(res => {
if (res.code === 200 && res.data) {
proxy.$modal.msgSuccess('保存成功');
} else {
proxy.$modal.msgError('保存失败');
}
});
} else {
// 调用API新增附加属性
addProductAddition(additionData).then(response => {
if (response.code === 200) {
proxy.$modal.msgSuccess('保存成功');
} else {
proxy.$modal.msgError('保存失败');
}
});
}
});
additionOpen.value = false;
} else {
proxy.$modal.msgError('保存失败');
}
}).catch(() => {
proxy.$modal.msgError('保存失败');
});
}
function handleLabor(row) {
@@ -725,38 +713,28 @@ function addLaborItem() {
}
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);
}
async function saveLabors() {
const valid = laborList.value
try {
const items = (laborList.value || [])
.map(item => ({
...item,
laborName: item.laborName ? String(item.laborName).trim() : ''
laborId: item?.laborId,
laborName: item?.laborName ? String(item.laborName).trim() : '',
laborPrice: item?.laborPrice ?? 0
}))
.filter(item => item.laborName);
const tasks = valid.map(item => {
const data = {
laborId: item.laborId,
const res = await batchSaveProductLabor({
productId: currentProductId.value,
laborName: item.laborName,
laborPrice: item.laborPrice ?? 0
};
if (data.laborId) return updateProductLabor(data);
return addProductLabor(data);
items
});
try {
await Promise.all(tasks);
if (res.code === 200 && res.data) {
proxy.$modal.msgSuccess('保存成功');
} else {
proxy.$modal.msgError('保存失败');
}
} catch (e) {
proxy.$modal.msgError('保存失败');
} finally {
@@ -787,4 +765,15 @@ getList();
:deep(.el-image-viewer__wrapper) {
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>

View File

@@ -74,17 +74,54 @@
</template>
</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="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">
<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>
</template>
</el-table-column>
</el-table>
<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>
</template>
@@ -167,13 +204,83 @@ function round2(v) {
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) {
const workload = toNumber(row.workload)
const unitPrice = row.billingType === '2' ? toNumber(row.unitPrice) : toNumber(row.unitPrice)
const extraAmount = toNumber(row.extraAmount)
const calcAmount = toNumber(row.calcAmount)
const baseAmount = round2(workload * unitPrice)
row.baseAmount = baseAmount
row.totalAmount = round2(baseAmount + extraAmount)
row.totalAmount = round2(baseAmount + extraAmount + calcAmount)
// 重新计算累计金额
updateCumulativeAmounts()
@@ -210,6 +317,8 @@ function getList() {
workload: normalizeEditableValue(row.workload),
unitPrice: normalizeEditableValue(row.unitPrice),
extraAmount: normalizeEditableValue(row.extraAmount),
calcAmount: row.calcAmount ?? 0,
calcDetail: row.calcDetail,
cumulativeAmount: cumulativeAmounts.value[row.empName] || 0
}
recalcRowAmount(r)
@@ -241,6 +350,9 @@ function buildRowPayload(row) {
if (payload.extraAmount === '' || payload.extraAmount === null || payload.extraAmount === undefined) {
payload.extraAmount = 0
}
if (payload.calcAmount === '' || payload.calcAmount === null || payload.calcAmount === undefined) {
payload.calcAmount = 0
}
if (payload.billingType !== '2') {
payload.unitPrice = null
} else if (payload.unitPrice === '' || payload.unitPrice === null || payload.unitPrice === undefined) {

View File

@@ -119,7 +119,9 @@ CREATE TABLE gear_wage_entry_detail (
base_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 '额外金额原因',
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是',
source_detail_id bigint(20) DEFAULT NULL COMMENT '被补录/被修改的原始明细ID',
makeup_responsible_id bigint(20) DEFAULT NULL COMMENT '补录责任人ID',