Files
GEAR-OA/gear-ui3/src/views/mat/product/detail.vue
2026-04-28 16:53:35 +08:00

530 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container product-detail">
<el-card class="detail-card" v-loading="loading">
<template #header>
<div class="card-header">
<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>
<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>
</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>
<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>
</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 { listProductLabor } from "@/api/mat/productLabor";
import { listByIds, listOss } from "@/api/system/oss";
import { formatDecimal } from '@/utils/gear';
import { Document } from '@element-plus/icons-vue';
const router = useRouter();
const route = useRoute();
const productDetail = ref({});
const loading = ref(true);
const materialLoading = ref(false);
const additionLoading = ref(false);
const pdfFiles = ref([]);
const pdfUrlFiles = ref([]);
// 材料明细数据
const productMaterialRelationList = ref([]);
// 产品附加属性数据
const productAdditionList = ref([]);
const productLaborList = 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 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 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 + manualLaborTotal;
});
const isOssIdList = (val) => {
if (val === null || val === undefined) return false;
const str = String(val).trim();
return /^[0-9]+(,[0-9]+)*$/.test(str);
};
const pdfDisplayList = computed(() => {
const raw = String(productDetail.value.productPdfs || '').trim();
if (!raw) return [];
if (isOssIdList(raw)) return pdfFiles.value;
return pdfUrlFiles.value;
});
// 获取产品详情
function getProductDetail() {
const productId = route.params.id;
if (productId) {
loading.value = true;
getProduct(productId).then(response => {
productDetail.value = response.data;
loading.value = false;
resolvePdfFiles();
// 获取材料明细
getMaterialDetail(productId);
// 获取产品附加属性
getProductAddition(productId);
getProductLabor(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 getProductLabor(productId) {
listProductLabor({ productId }).then(response => {
productLaborList.value = response.rows || [];
}).catch(() => {
productLaborList.value = [];
});
}
function resolvePdfFiles() {
const raw = String(productDetail.value.productPdfs || '').trim();
if (!raw) {
pdfFiles.value = [];
pdfUrlFiles.value = [];
return;
}
if (isOssIdList(raw)) {
pdfUrlFiles.value = [];
listByIds(raw).then(res => {
const list = res.data || [];
pdfFiles.value = list.map(oss => ({
name: oss.originalName || oss.fileName || String(oss.ossId),
url: oss.url,
ossId: oss.ossId
}));
}).catch(() => {
pdfFiles.value = [];
});
return;
}
pdfFiles.value = [];
const urls = raw.split(',').map(s => String(s).trim()).filter(Boolean);
Promise.all(urls.map((url) =>
listOss({ url, pageNum: 1, pageSize: 1 })
.then(r => (r && r.rows && r.rows[0]) ? r.rows[0] : null)
.catch(() => null)
)).then(rows => {
pdfUrlFiles.value = urls.map((url, index) => {
const row = rows[index];
return {
name: (row && (row.originalName || row.fileName)) || getFileName(url),
url: (row && row.url) || url,
ossId: row && row.ossId
};
});
}).catch(() => {
pdfUrlFiles.value = urls.map(url => ({ name: getFileName(url), url }));
});
}
// 返回列表
function handleBack() {
router.back();
}
// 获取文件名
function getFileName(url) {
if (!url) return '';
return url.substring(url.lastIndexOf('/') + 1);
}
// 预览PDF
function previewPdf(pdf) {
if (!pdf || !pdf.url) return;
window.open(pdf.url, '_blank');
}
// 下载PDF
function downloadPdf(pdf) {
if (!pdf || !pdf.url) return;
const link = document.createElement('a');
link.href = pdf.url;
link.download = pdf.name || getFileName(pdf.url);
link.click();
}
onMounted(() => {
getProductDetail();
});
</script>
<style scoped>
.detail-card {
margin-bottom: 16px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
display: flex;
flex-direction: column;
gap: 4px;
}
.product-name {
font-weight: 600;
font-size: 16px;
line-height: 22px;
}
.sub-title {
color: var(--el-text-color-secondary);
font-size: 12px;
line-height: 18px;
}
.detail-desc :deep(.el-descriptions__label) {
width: 88px;
}
.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;
}
.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 {
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;
min-width: 0;
}
.pdf-icon {
font-size: 24px;
color: #409eff;
}
.pdf-meta {
min-width: 0;
}
.pdf-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 320px;
}
.pdf-sub {
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 18px;
}
.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: var(--el-fill-color-light);
border-top: 1px solid var(--el-border-color-light);
margin-top: -1px;
font-weight: bold;
}
</style>