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.

509 lines
13 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-button>
<el-button type="danger" size="small" @click="deleteSelectedRows" :disabled="!selectedRows.length">删除</el-button>
</div>
</div>
</template>
<el-table
:data="allMaterialData"
border
style="width: 100%"
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="materialName" label="材料名称" width="200">
<template #default="scope">
<template v-if="!scope.row.isOtherMaterial">
<el-input v-model="scope.row.materialName" placeholder="请输入材料名称" />
</template>
<template v-else>
其他材料费
</template>
</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="160">
<template #default="scope">
<el-select
v-model="scope.row.unit"
placeholder="单位"
filterable
allow-create
default-first-option
style="width: 100%"
clearable
>
<el-option
v-for="option in unitOptions"
:key="option"
:label="option"
:value="option"
/>
</el-select>
</template>
</el-table-column>
<el-table-column prop="unitPrice" label="单价(元/单位数量)" width="180">
<template #default="scope">
<el-input
v-model.number="scope.row.unitPrice"
type="number"
placeholder="单价"
@change="calculateAmount"
/>
</template>
</el-table-column>
<el-table-column prop="quantity" label="购置数量" width="100">
<template #default="scope">
<el-input
v-model.number="scope.row.quantity"
type="number"
placeholder="购置数量"
@change="calculateAmount"
/>
</template>
</el-table-column>
<el-table-column prop="amount" label="金额(万元)" width="120">
<template #default="scope">
<el-input v-model.number="scope.row.amount" type="number" placeholder="金额" disabled />
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="scope">
<el-button
v-if="!scope.row.isOtherMaterial"
type="danger"
size="small"
@click="deleteRow(scope.row.id)"
>
删除
</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>主要材料费小计: {{ mainMaterialTotalAmount }} </span>
<span class="font-semibold">合计: {{ 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'
// 接收项目ID作为props
const props = defineProps<{
projectId: string
}>()
// 定义事件
const emit = defineEmits<{
update: [data: { materialCost: number }]
}>()
// 材料费数据
const materialData = reactive<any[]>([])
// 单位下拉列表选项
const unitOptions = ref([
'kg', 'g', 't', 'm', 'cm', 'mm', 'm²', 'm³',
'个', '件', '套', '批', '箱', '袋', '瓶',
'小时', '天', '月', '年', '次'
]);
// 其他材料费
const otherMaterial = reactive({
id: -1,
isOtherMaterial: true,
materialName: '其他材料费',
unit: '',
quantity: 0,
unitPrice: 0,
amount: 0
})
// 合并所有材料数据(主要材料 + 其他材料)
const allMaterialData = computed(() => {
return [...materialData, otherMaterial]
})
// 总金额和总数量
const totalAmount = ref(0)
const totalQuantity = ref(0)
// 主要材料费合计(排除其他材料费)
const mainMaterialTotalAmount = computed(() => {
return materialData.reduce((sum, item) => sum + (item.amount || 0), 0)
})
const mainMaterialTotalQuantity = computed(() => {
return materialData.reduce((sum, item) => sum + (item.quantity || 0), 0)
})
// 选中的行
const selectedRows = ref<any[]>([])
// 添加行数
const addRowCount = ref(1)
// ID计数器
let idCounter = 1
// 监听项目ID变化
watch(() => props.projectId, (newProjectId) => {
if (newProjectId) {
loadMaterialData(newProjectId)
}
}, { immediate: true })
// 监听材料数据变化,更新总合计
watch(() => [materialData, otherMaterial], () => {
calculateTotal()
}, { deep: true })
// 加载材料费数据
const loadMaterialData = async (projectId: string) => {
try {
// 这里应该调用API获取材料费数据暂时使用模拟数据
console.log('加载材料费数据:', projectId)
// 清空现有数据
materialData.splice(0, materialData.length)
// 模拟数据
const mockData = [
{
id: idCounter++,
materialName: '特种合金材料',
specification: '型号X-50',
unit: 'kg',
quantity: 100,
unitPrice: 500,
amount: 50000,
usage: '样品制作',
remarks: ''
},
{
id: idCounter++,
materialName: '电子元件包',
specification: '套装',
unit: '套',
quantity: 5,
unitPrice: 8000,
amount: 40000,
usage: '电路开发',
remarks: ''
},
{
id: idCounter++,
materialName: '包装材料',
specification: '标准型',
unit: '批',
quantity: 2,
unitPrice: 10000,
amount: 20000,
usage: '样品包装',
remarks: ''
},
{
id: idCounter++,
materialName: '实验试剂',
specification: '分析纯',
unit: '瓶',
quantity: 20,
unitPrice: 2000,
amount: 40000,
usage: '性能测试',
remarks: ''
}
]
// 添加到响应式数组
mockData.forEach(item => materialData.push(item))
// 计算总金额
calculateTotal()
} catch (error) {
ElMessage.error('加载材料费数据失败')
console.error('加载材料费数据失败:', error)
}
}
// 添加指定行数
const addSpecifiedRows = async () => {
if (!addRowCount.value || addRowCount.value <= 0) {
ElMessage.warning('请输入有效的行数')
return
}
// 添加指定行数
for (let i = 0; i < addRowCount.value; i++) {
materialData.push({
id: idCounter++,
materialName: '',
specification: '',
unit: '',
quantity: 0,
unitPrice: 0,
amount: 0,
usage: '',
remarks: ''
})
}
// 等待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 deleteSelectedRows = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要删除的行')
return
}
ElMessageBox.confirm(
`确定要删除选中的 ${selectedRows.value.length} 行吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
// 获取选中行的ID
const selectedIds = selectedRows.value.map(row => row.id)
// 删除选中的行
const newData = materialData.filter(item => !selectedIds.includes(item.id))
materialData.splice(0, materialData.length, ...newData)
// 清空选中状态
selectedRows.value = []
// 计算总金额
calculateTotal()
ElMessage.success('删除成功')
}).catch(() => {
// 取消删除
})
}
// 删除单行
const deleteRow = (id: number) => {
ElMessageBox.confirm(
'确定要删除这一行吗?',
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
const index = materialData.findIndex(item => item.id === id)
if (index > -1) {
materialData.splice(index, 1)
calculateTotal()
ElMessage.success('删除成功')
}
}).catch(() => {
// 取消删除
})
}
// 计算单项金额
const calculateAmount = () => {
// 计算主要材料金额
materialData.forEach(item => {
item.amount = (item.quantity || 0) * (item.unitPrice || 0)
})
// 计算其他材料金额
otherMaterial.amount = (otherMaterial.quantity || 0) * (otherMaterial.unitPrice || 0)
// 计算总金额
calculateTotal()
}
// 计算总金额和总数量
const calculateTotal = () => {
totalAmount.value = mainMaterialTotalAmount.value + otherMaterial.amount
totalQuantity.value = mainMaterialTotalQuantity.value + (otherMaterial.quantity || 0)
// 触发update事件通知父组件
emit('update', { materialCost: totalAmount.value })
}
// 刷新数据
const refreshData = () => {
if (props.projectId) {
loadMaterialData(props.projectId)
}
}
// 重置数据
const resetData = () => {
materialData.splice(0, materialData.length)
totalAmount.value = 0
totalQuantity.value = 0
idCounter = 1
// 重置其他材料费
otherMaterial.unit = ''
otherMaterial.quantity = 0
otherMaterial.unitPrice = 0
otherMaterial.amount = 0
}
// 获取表单数据
const getFormData = () => {
return {
materialData: [...materialData],
otherMaterial: { ...otherMaterial },
totalAmount: totalAmount.value,
totalQuantity: totalQuantity.value
}
}
// 暴露方法给父组件
defineExpose({
refreshData,
resetData,
getFormData
})
</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>