明细计算,产品详情页
This commit is contained in:
@@ -34,3 +34,11 @@ export function delProductAddition(addId) {
|
||||
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>
|
||||
<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>
|
||||
</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 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
|
||||
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>
|
||||
</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="120" align="center">
|
||||
<template #default="scope">{{ formatDecimal(scope.row.subtotal) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="section-summary">
|
||||
主材小计:{{ formatDecimal(mainMaterials.reduce((sum, item) => sum + item.subtotal, 0)) }} 元
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>材料明细</span>
|
||||
</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>
|
||||
</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 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="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>
|
||||
</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 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>
|
||||
|
||||
@@ -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>
|
||||
<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="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>
|
||||
<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" 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());
|
||||
|
||||
// 保存附加属性
|
||||
validAdditions.forEach(item => {
|
||||
const additionData = {
|
||||
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) {
|
||||
proxy.$modal.msgSuccess('保存成功');
|
||||
} else {
|
||||
proxy.$modal.msgError('保存失败');
|
||||
}
|
||||
});
|
||||
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);
|
||||
|
||||
batchSaveProductAddition({
|
||||
productId: currentProductId.value,
|
||||
items
|
||||
}).then(res => {
|
||||
if (res.code === 200 && res.data) {
|
||||
proxy.$modal.msgSuccess('保存成功');
|
||||
additionOpen.value = false;
|
||||
} else {
|
||||
// 调用API新增附加属性
|
||||
addProductAddition(additionData).then(response => {
|
||||
if (response.code === 200) {
|
||||
proxy.$modal.msgSuccess('保存成功');
|
||||
} else {
|
||||
proxy.$modal.msgError('保存失败');
|
||||
}
|
||||
});
|
||||
proxy.$modal.msgError('保存失败');
|
||||
}
|
||||
}).catch(() => {
|
||||
proxy.$modal.msgError('保存失败');
|
||||
});
|
||||
|
||||
additionOpen.value = false;
|
||||
}
|
||||
|
||||
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
|
||||
.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 {
|
||||
await Promise.all(tasks);
|
||||
proxy.$modal.msgSuccess('保存成功');
|
||||
const items = (laborList.value || [])
|
||||
.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) {
|
||||
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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user