You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
449 lines
12 KiB
Vue
449 lines
12 KiB
Vue
<template>
|
|
<div class="material-cost-container">
|
|
<el-card class="table-card">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>材料费明细</span>
|
|
<div class="header-buttons">
|
|
<el-input-number v-model="addRowCount" :min="1" :max="100" :step="1" size="small" style="width: 100px; margin-right: 10px" />
|
|
<el-button type="primary" size="small" @click="addSpecifiedRows">
|
|
<el-icon>
|
|
<Plus />
|
|
</el-icon>
|
|
添加
|
|
</el-button>
|
|
<el-button type="danger" size="small" @click="handleBatchDelete" :disabled="!selectedRows.length">
|
|
<el-icon>
|
|
<Delete />
|
|
</el-icon>
|
|
删除
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<el-table :data="allMaterialData" border style="width: 100%" @selection-change="handleSelectionChange">
|
|
<el-table-column type="selection" width="55" :selectable="checkSelectable" />
|
|
<el-table-column prop="sortOrder" label="序号" width="80" align="center" />
|
|
<el-table-column prop="materialName" label="材料名称" min-width="200">
|
|
<template #default="scope">
|
|
<el-input v-model="scope.row.materialName" placeholder="请输入材料名称" :disabled="scope.row.materialName === '其他材料费'" />
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column prop="unitId" label="单位" width="160">
|
|
<template #default="scope">
|
|
<el-select v-model="scope.row.unitId" placeholder="单位" filterable allow-create default-first-option style="width: 100%" clearable>
|
|
<el-option v-for="option in unitOptions" :key="option.unitId" :label="option.unitName" :value="option.unitId" />
|
|
</el-select>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="unitPrice" label="单价(元/单位数量)" width="180">
|
|
<template #default="scope">
|
|
<el-input-number v-model.number="scope.row.unitPrice" placeholder="单价" :min="0" :precision="2" size="small" @change="calculateAmount" />
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="amount" label="购置数量" width="180">
|
|
<template #default="scope">
|
|
<el-input-number
|
|
v-model.number="scope.row.amount"
|
|
placeholder="购置数量"
|
|
:min="0"
|
|
:precision="2"
|
|
size="small"
|
|
@change="calculateAmount"
|
|
/>
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column prop="price" label="金额(万元)" width="120">
|
|
<template #default="scope">
|
|
{{ format2TenThousandNumber(scope.row.price) }}
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column label="操作" width="100" fixed="right">
|
|
<template #default="scope">
|
|
<el-button
|
|
v-if="scope.row.materialType === MATERIAL_TYPE.MAIN"
|
|
type="danger"
|
|
size="small"
|
|
@click="handleDelete(scope.$index, scope.row)"
|
|
icon="Delete"
|
|
>
|
|
删除
|
|
</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<!-- 底部合计行 -->
|
|
<!--
|
|
<div class="table-totals">
|
|
<table class="total-table">
|
|
<tr class="total-row">
|
|
<td rowspan="2" class="checkbox-col"></td>
|
|
<td rowspan="2" class="index-col"></td>
|
|
<td class="merged-cell">主要材料费小计</td>
|
|
<td class="empty-cell"></td>
|
|
<td class="empty-cell"></td>
|
|
<td class="total-value">{{ mainMaterialTotalQuantity }}</td>
|
|
<td class="empty-cell"></td>
|
|
<td class="total-value">{{ mainMaterialTotalAmount }}</td>
|
|
<td class="empty-cell"></td>
|
|
<td class="empty-cell"></td>
|
|
<td class="empty-cell"></td>
|
|
</tr>
|
|
|
|
<tr class="total-row total-final">
|
|
<td class="merged-cell">合计</td>
|
|
<td class="empty-cell"></td>
|
|
<td class="empty-cell"></td>
|
|
<td class="total-value total-bold">{{ totalQuantity }}</td>
|
|
<td class="empty-cell"></td>
|
|
<td class="total-value total-bold">{{ totalAmount }}</td>
|
|
<td class="empty-cell"></td>
|
|
<td class="empty-cell"></td>
|
|
<td class="empty-cell"></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
-->
|
|
<div class="mt-4 bg-gray-50 p-3 rounded">
|
|
<div class="flex justify-end gap-8">
|
|
<span>主要材料费小计: {{ format2TenThousandNumber(mainMaterialTotalAmount) }} 万元</span>
|
|
<span class="font-semibold">合计: {{ formatNumber(totalAmount) }} 万元</span>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, watch, computed, nextTick } from 'vue';
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
import { rdBudgetMaterialCostVO } from '@/api/oa/erp/budgetInfo/rd/rdBudgetMaterialCost/types';
|
|
import { getBaseUnitInfoList } from '@/api/oa/base/unitInfo';
|
|
import { UnitInfoVO } from '@/api/oa/base/unitInfo/types';
|
|
|
|
// 接收项目ID作为props
|
|
const props = defineProps<{
|
|
projectId: string;
|
|
}>();
|
|
|
|
const MATERIAL_TYPE = {
|
|
MAIN: '1', //主要材料费
|
|
OTHER: '2' //其他材料费
|
|
};
|
|
|
|
// 材料费数据
|
|
const materialData = ref<rdBudgetMaterialCostVO[]>([]);
|
|
const toDeletedMaterialCostIdList = ref([]);
|
|
|
|
// 其他材料费
|
|
const otherMaterialData = ref<rdBudgetMaterialCostVO>();
|
|
|
|
const defaultOtherMaterial = ref({
|
|
sortOrder: 1,
|
|
materialName: '其他材料费',
|
|
materialType: MATERIAL_TYPE.OTHER,
|
|
unitId: undefined,
|
|
unitPrice: undefined,
|
|
amount: undefined,
|
|
price: 0
|
|
});
|
|
// 响应式数据
|
|
const otherMaterial = ref(defaultOtherMaterial);
|
|
|
|
// 合并所有材料数据(主要材料 + 其他材料)
|
|
const allMaterialData = computed(() => {
|
|
return [...materialData.value, otherMaterial.value];
|
|
});
|
|
|
|
const unitOptions = ref<UnitInfoVO[]>([]);
|
|
|
|
// 单位下拉列表选项
|
|
// const unitOptions = ref(['kg', 'g', 't', 'm', 'cm', 'mm', 'm²', 'm³', '个', '件', '套', '批', '箱', '袋', '瓶', '小时', '天', '月', '年', '次']);
|
|
|
|
// 总金额和总数量
|
|
const totalAmount = ref(0);
|
|
const totalQuantity = ref(0);
|
|
|
|
// 主要材料费合计(排除其他材料费)
|
|
const mainMaterialTotalAmount = computed(() => {
|
|
return materialData.value.reduce((sum, item) => sum + (Number(item.price) || 0), 0);
|
|
});
|
|
|
|
// 选中的行
|
|
const selectedRows = ref<any[]>([]);
|
|
|
|
// 添加行数
|
|
const addRowCount = ref(1);
|
|
|
|
// 格式化数字,元转换为万元
|
|
const formatNumber = (value: number) => {
|
|
if (!value) return '0.00';
|
|
return parseFloat(value).toFixed(2);
|
|
};
|
|
|
|
// 格式化数字,元转换为万元
|
|
const format2TenThousandNumber = (value: number) => {
|
|
if (!value) return '0.00';
|
|
return (parseFloat(value) / 10000).toFixed(2);
|
|
};
|
|
|
|
const checkSelectable = (row: rdBudgetMaterialCostVO, index: number) => {
|
|
return row.materialType === MATERIAL_TYPE.MAIN;
|
|
};
|
|
|
|
// 监听 otherMaterialData 的变化
|
|
watch(
|
|
otherMaterialData,
|
|
(newVal) => {
|
|
if (newVal) {
|
|
otherMaterial.value = newVal;
|
|
} else {
|
|
otherMaterial.value = defaultOtherMaterial.value;
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
); // immediate: true 立即执行一次
|
|
|
|
// 监听材料数据变化,更新总合计
|
|
watch(
|
|
() => [materialData, otherMaterial],
|
|
() => {
|
|
calculateTotal();
|
|
},
|
|
{ deep: true }
|
|
);
|
|
|
|
// 添加指定行数
|
|
const addSpecifiedRows = async () => {
|
|
if (!addRowCount.value || addRowCount.value <= 0) {
|
|
ElMessage.warning('请输入有效的行数');
|
|
return;
|
|
}
|
|
const currentSortOrder = materialData.value && materialData.value.length > 0 ? Math.max(...materialData.value.map((item) => item.sortOrder)) : 0;
|
|
|
|
// 添加指定行数
|
|
for (let i = 0; i < addRowCount.value; i++) {
|
|
materialData.value.push({
|
|
sortOrder: currentSortOrder + i + 1,
|
|
materialName: '',
|
|
materialType: MATERIAL_TYPE.MAIN,
|
|
unitId: undefined,
|
|
unitPrice: undefined,
|
|
amount: undefined,
|
|
price: 0
|
|
});
|
|
}
|
|
otherMaterial.value.sortOrder = currentSortOrder + 1 + addRowCount.value;
|
|
// 等待DOM更新后滚动到底部
|
|
await nextTick();
|
|
scrollToBottom();
|
|
};
|
|
|
|
// 滚动到底部
|
|
const scrollToBottom = () => {
|
|
const tableContainer = document.querySelector('.el-table__body-wrapper');
|
|
if (tableContainer) {
|
|
tableContainer.scrollTop = tableContainer.scrollHeight;
|
|
}
|
|
};
|
|
|
|
// 处理选择变化
|
|
const handleSelectionChange = (val: any[]) => {
|
|
// 过滤掉其他材料费行
|
|
selectedRows.value = val.filter((row) => !row.isOtherMaterial);
|
|
};
|
|
|
|
// 删除单行
|
|
const handleDelete = (index: number, row: rdBudgetMaterialCostVO) => {
|
|
materialData.value.splice(index, 1);
|
|
// 重新编号
|
|
materialData.value.forEach((item, index) => {
|
|
item.sortOrder = index + 1;
|
|
});
|
|
if (row.materialCostId) {
|
|
toDeletedMaterialCostIdList.value.push(row.materialCostId);
|
|
}
|
|
otherMaterial.value.sortOrder = otherMaterial.value.sortOrder - 1;
|
|
calculateTotal();
|
|
};
|
|
|
|
// 批量删除
|
|
const handleBatchDelete = () => {
|
|
if (selectedRows.value.length === 0) {
|
|
ElMessage.warning('请选择要删除的行');
|
|
return;
|
|
}
|
|
selectedRows.value.forEach((selectedRow) => {
|
|
if (selectedRow.materialCostId) {
|
|
toDeletedMaterialCostIdList.value.push(selectedRow.materialCostId);
|
|
}
|
|
});
|
|
|
|
materialData.value = materialData.value.filter((item) => !selectedRows.value.includes(item));
|
|
// 重新编号
|
|
materialData.value.forEach((item, index) => {
|
|
item.sortOrder = index + 1;
|
|
});
|
|
otherMaterial.value.sortOrder = otherMaterial.value.sortOrder - selectedRows.value.length;
|
|
calculateTotal();
|
|
selectedRows.value = [];
|
|
};
|
|
|
|
// 计算单项金额
|
|
const calculateAmount = () => {
|
|
// 计算主要材料金额
|
|
materialData.value.forEach((item) => {
|
|
item.price = (item.amount || 0) * (item.unitPrice || 0);
|
|
});
|
|
|
|
// 计算其他材料金额
|
|
otherMaterial.value.price = (otherMaterial.value.amount || 0) * (otherMaterial.value.unitPrice || 0);
|
|
|
|
// 计算总金额
|
|
calculateTotal();
|
|
};
|
|
|
|
// 计算总金额和总数量
|
|
const calculateTotal = () => {
|
|
const mainAmount = parseFloat(((Number(mainMaterialTotalAmount.value) || 0) / 10000).toFixed(2));
|
|
const otherAmount = parseFloat(((Number(otherMaterial.value.price) || 0) / 10000).toFixed(2));
|
|
totalAmount.value = (mainAmount + otherAmount).toFixed(2);
|
|
};
|
|
|
|
// 获取总金额(供父组件调用)
|
|
const getTotalAmount = () => {
|
|
return (Number(totalAmount.value) || 0) * 10000;
|
|
};
|
|
|
|
// 获取单位列表
|
|
const getUnitInfoList = async () => {
|
|
const res = await getBaseUnitInfoList({});
|
|
unitOptions.value = res.data;
|
|
};
|
|
|
|
onMounted(() => {
|
|
getUnitInfoList();
|
|
});
|
|
|
|
// 刷新数据
|
|
const refreshData = () => {
|
|
if (props.projectId) {
|
|
}
|
|
};
|
|
|
|
// 重置数据
|
|
const resetData = () => {
|
|
materialData.value.splice(0, materialData.value.length);
|
|
totalAmount.value = 0;
|
|
totalQuantity.value = 0;
|
|
|
|
// 重置其他材料费
|
|
// otherMaterial.unit = '';
|
|
// otherMaterial.quantity = 0;
|
|
// otherMaterial.unitPrice = 0;
|
|
// otherMaterial.amount = 0;
|
|
};
|
|
|
|
// 获取表单数据
|
|
const getFormData = () => {
|
|
return {
|
|
materialData: [...materialData.value],
|
|
otherMaterial: { ...otherMaterial },
|
|
totalAmount: totalAmount.value,
|
|
totalQuantity: totalQuantity.value
|
|
};
|
|
};
|
|
|
|
// 暴露方法给父组件
|
|
defineExpose({
|
|
materialData,
|
|
otherMaterial,
|
|
otherMaterialData,
|
|
allMaterialData,
|
|
toDeletedMaterialCostIdList,
|
|
getTotalAmount
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.material-cost-container {
|
|
padding: 10px;
|
|
}
|
|
|
|
.table-card {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.header-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
/* 表格底部合计样式 */
|
|
.table-totals {
|
|
margin-top: -1px; /* 与表格边框重叠,避免双线 */
|
|
}
|
|
|
|
.total-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
border: 1px solid #ebeef5;
|
|
border-top: none;
|
|
}
|
|
|
|
.total-row td {
|
|
border: 1px solid #ebeef5;
|
|
padding: 12px;
|
|
text-align: center;
|
|
}
|
|
|
|
.total-row:first-child {
|
|
background-color: #fafafa;
|
|
}
|
|
|
|
.total-final {
|
|
background-color: #f5f7fa;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* 列宽设置,与表格列对齐 */
|
|
.checkbox-col {
|
|
width: 55px;
|
|
}
|
|
|
|
.index-col {
|
|
width: 60px;
|
|
}
|
|
|
|
.merged-cell {
|
|
width: 200px;
|
|
text-align: left;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.empty-cell {
|
|
width: 180px;
|
|
}
|
|
|
|
.total-value {
|
|
width: 120px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.total-bold {
|
|
color: #f56c6c;
|
|
font-size: 16px;
|
|
}
|
|
</style>
|