360 lines
11 KiB
Vue
360 lines
11 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="app-container">
|
|||
|
|
<el-card class="mb20">
|
|||
|
|
<template #header>
|
|||
|
|
<div class="card-header">
|
|||
|
|
<span>{{ productDetail.productName }} - 产品详情</span>
|
|||
|
|
<el-button type="primary" plain size="small" @click="handleBack">返回列表</el-button>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
<div class="product-info">
|
|||
|
|
<div class="info-item">
|
|||
|
|
<span class="label">产品名称:</span>
|
|||
|
|
<span class="value">{{ productDetail.productName }}</span>
|
|||
|
|
</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>
|
|||
|
|
</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>
|
|||
|
|
</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="total-section">
|
|||
|
|
<div class="total-item">
|
|||
|
|
<span class="total-label">合计:</span>
|
|||
|
|
<span class="total-value">{{ formatDecimal(totalAmount) }} 元</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</el-card>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup name="ProductDetail">
|
|||
|
|
import { ref, computed, onMounted } from 'vue';
|
|||
|
|
import { useRouter, useRoute } from 'vue-router';
|
|||
|
|
import { getProduct } from "@/api/mat/product";
|
|||
|
|
import { listProductMaterialRelation } from "@/api/mat/productMaterialRelation";
|
|||
|
|
import { getMaterial } from "@/api/mat/material";
|
|||
|
|
import { listProductAddition } from "@/api/mat/productAddition";
|
|||
|
|
import { formatDecimal } from '@/utils/gear';
|
|||
|
|
|
|||
|
|
const router = useRouter();
|
|||
|
|
const route = useRoute();
|
|||
|
|
|
|||
|
|
const productDetail = ref({});
|
|||
|
|
const loading = ref(true);
|
|||
|
|
const materialLoading = ref(false);
|
|||
|
|
const additionLoading = ref(false);
|
|||
|
|
|
|||
|
|
// 材料明细数据
|
|||
|
|
const productMaterialRelationList = ref([]);
|
|||
|
|
|
|||
|
|
// 产品附加属性数据
|
|||
|
|
const productAdditionList = ref([]);
|
|||
|
|
|
|||
|
|
// 计算主材、辅材和工本
|
|||
|
|
const mainMaterials = computed(() => {
|
|||
|
|
// 主材(材料类型为2)
|
|||
|
|
return productMaterialRelationList.value
|
|||
|
|
.filter(item => item.material && item.material.materialType === 2)
|
|||
|
|
.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 auxiliaryMaterials = computed(() => {
|
|||
|
|
// 辅材(材料类型为1)
|
|||
|
|
return productMaterialRelationList.value
|
|||
|
|
.filter(item => item.material && item.material.materialType === 1)
|
|||
|
|
.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 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);
|
|||
|
|
return mainTotal + auxiliaryTotal + laborTotal;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 获取产品详情
|
|||
|
|
function getProductDetail() {
|
|||
|
|
const productId = route.params.id;
|
|||
|
|
if (productId) {
|
|||
|
|
loading.value = true;
|
|||
|
|
getProduct(productId).then(response => {
|
|||
|
|
productDetail.value = response.data;
|
|||
|
|
loading.value = false;
|
|||
|
|
// 获取材料明细
|
|||
|
|
getMaterialDetail(productId);
|
|||
|
|
// 获取产品附加属性
|
|||
|
|
getProductAddition(productId);
|
|||
|
|
}).catch(error => {
|
|||
|
|
console.error('获取产品详情失败:', error);
|
|||
|
|
loading.value = false;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取产品附加属性
|
|||
|
|
function getProductAddition(productId) {
|
|||
|
|
additionLoading.value = true;
|
|||
|
|
listProductAddition({ productId }).then(response => {
|
|||
|
|
productAdditionList.value = response.rows || [];
|
|||
|
|
additionLoading.value = false;
|
|||
|
|
}).catch(error => {
|
|||
|
|
console.error('获取产品附加属性失败:', error);
|
|||
|
|
additionLoading.value = false;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取材料明细
|
|||
|
|
function getMaterialDetail(productId) {
|
|||
|
|
materialLoading.value = true;
|
|||
|
|
listProductMaterialRelation({ productId }).then(response => {
|
|||
|
|
const relations = response.rows;
|
|||
|
|
// 为每个配方项获取物料详细信息,包括物料类型
|
|||
|
|
const promises = relations.map(item => {
|
|||
|
|
return getMaterial(item.materialId).then(materialResponse => {
|
|||
|
|
item.material = materialResponse.data;
|
|||
|
|
return item;
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
return Promise.all(promises);
|
|||
|
|
}).then(relationsWithMaterial => {
|
|||
|
|
productMaterialRelationList.value = relationsWithMaterial;
|
|||
|
|
materialLoading.value = false;
|
|||
|
|
}).catch(error => {
|
|||
|
|
console.error('获取材料明细失败:', error);
|
|||
|
|
materialLoading.value = false;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 返回列表
|
|||
|
|
function handleBack() {
|
|||
|
|
router.back();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
getProductDetail();
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.app-container {
|
|||
|
|
padding: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.product-info {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 20px;
|
|||
|
|
margin: 20px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-item {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.label {
|
|||
|
|
font-weight: bold;
|
|||
|
|
min-width: 80px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.product-images {
|
|||
|
|
margin-top: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-list {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 10px;
|
|||
|
|
margin-top: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.material-section {
|
|||
|
|
margin: 20px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.section-title {
|
|||
|
|
background-color: #f5f7fa;
|
|||
|
|
padding: 10px;
|
|||
|
|
border-left: 4px solid #409eff;
|
|||
|
|
margin-bottom: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.section-summary {
|
|||
|
|
text-align: right;
|
|||
|
|
padding: 10px;
|
|||
|
|
background-color: #f9f9f9;
|
|||
|
|
border-top: 1px solid #e4e7ed;
|
|||
|
|
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>
|