明细计算,产品详情页

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

@@ -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>
</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>

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>
<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>

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) {