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.

381 lines
13 KiB
Vue

<template>
<div class="travel-cost">
<!-- 标题 -->
<div class="title">差旅费预算明细表</div>
<!-- 操作按钮 -->
<div class="actions">
<el-button type="primary" @click="showAddDialog = true">添加</el-button>
<el-button type="danger" @click="handleBatchDelete"></el-button>
</div>
<!-- 表格 -->
<el-table
:data="travelData"
border
style="width: 100%"
:summary-method="getSummaries"
show-summary
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<!-- 预算成本 -->
<el-table-column label="预算成本" align="center">
<el-table-column prop="serialNumber" label="序号" width="80" align="center" />
<el-table-column prop="destination" label="出差地点" width="120">
<template #default="scope">
<el-input v-model="scope.row.destination" />
</template>
</el-table-column>
<el-table-column prop="purpose" label="事由" width="150">
<template #default="scope">
<el-input v-model="scope.row.purpose" type="textarea" :rows="2" />
</template>
</el-table-column>
<el-table-column prop="times" label="次数" width="80" align="right">
<template #default="scope">
<el-input v-model="scope.row.times" @input="calculateBudgetSubtotal(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="personCount" label="人数" width="80" align="right">
<template #default="scope">
<el-input v-model="scope.row.personCount" @input="calculateBudgetSubtotal(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="days" label="天数" width="80" align="right">
<template #default="scope">
<el-input v-model="scope.row.days" @input="calculateBudgetSubtotal(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="accommodationStandard" label="住宿标准(元)" width="120" align="right">
<template #default="scope">
<el-input v-model="scope.row.accommodationStandard" @input="calculateBudgetSubtotal(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="roundTripFee" label="往返路费(元)" width="120" align="right">
<template #default="scope">
<el-input v-model="scope.row.roundTripFee" @input="calculateBudgetSubtotal(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="accommodationFee" label="住宿费(元)" width="120" align="right">
<template #default="scope">
{{ formatNumber(scope.row.accommodationFee) }}
</template>
</el-table-column>
<el-table-column prop="subsidy" label="补贴(元)" width="120" align="right">
<template #default="scope">
{{ formatNumber(scope.row.subsidy) }}
</template>
</el-table-column>
<el-table-column prop="budgetSubtotal" label="小计(万元)" width="120" align="right">
<template #default="scope">
{{ formatNumber(scope.row.budgetSubtotal) }}
</template>
</el-table-column>
</el-table-column>
<!-- 降成本后预算成本 -->
<el-table-column label="降成本后预算成本" align="center">
<el-table-column prop="reducedSerialNumber" label="序号" width="80" align="center" />
<el-table-column prop="reducedDestination" label="出差地点" width="120">
<template #default="scope">
<el-input v-model="scope.row.reducedDestination" />
</template>
</el-table-column>
<el-table-column prop="reducedPurpose" label="事由" width="150">
<template #default="scope">
<el-input v-model="scope.row.reducedPurpose" type="textarea" :rows="2" />
</template>
</el-table-column>
<el-table-column prop="reducedTimes" label="次数" width="80" align="right">
<template #default="scope">
<el-input v-model="scope.row.reducedTimes" @input="calculateReducedSubtotal(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="reducedPersonCount" label="人数" width="80" align="right">
<template #default="scope">
<el-input v-model="scope.row.reducedPersonCount" @input="calculateReducedSubtotal(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="reducedDays" label="天数" width="80" align="right">
<template #default="scope">
<el-input v-model="scope.row.reducedDays" @input="calculateReducedSubtotal(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="reducedAccommodationStandard" label="住宿标准(元)" width="120" align="right">
<template #default="scope">
<el-input v-model="scope.row.reducedAccommodationStandard" @input="calculateReducedSubtotal(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="reducedRoundTripFee" label="往返路费(元)" width="120" align="right">
<template #default="scope">
<el-input v-model="scope.row.reducedRoundTripFee" @input="calculateReducedSubtotal(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="reducedAccommodationFee" label="住宿费(元)" width="120" align="right">
<template #default="scope">
{{ formatNumber(scope.row.reducedAccommodationFee) }}
</template>
</el-table-column>
<el-table-column prop="reducedSubsidy" label="补贴(元)" width="120" align="right">
<template #default="scope">
{{ formatNumber(scope.row.reducedSubsidy) }}
</template>
</el-table-column>
<el-table-column prop="reducedSubtotal" label="小计(万元)" width="120" align="right">
<template #default="scope">
{{ formatNumber(scope.row.reducedSubtotal) }}
</template>
</el-table-column>
</el-table-column>
<el-table-column prop="costReductionPlan" label="降成本方案" width="200">
<template #default="scope">
<el-input v-model="scope.row.costReductionPlan" type="textarea" :rows="2" />
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="scope">
<el-button type="danger" size="small" @click="handleDelete(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加行数弹窗 -->
<el-dialog
v-model="showAddDialog"
title="添加行数"
width="400px"
>
<el-form>
<el-form-item label="添加行数">
<el-input-number v-model="addRowCount" :min="1" :max="10" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddDialog = false">取消</el-button>
<el-button type="primary" @click="handleAddRows"></el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 差旅费数据
const travelData = ref([
{
serialNumber: 1,
destination: '',
purpose: '',
times: 0,
personCount: 0,
days: 0,
accommodationStandard: 0,
roundTripFee: 0,
accommodationFee: 0,
subsidy: 0,
budgetSubtotal: 0,
reducedSerialNumber: 1,
reducedDestination: '',
reducedPurpose: '',
reducedTimes: 0,
reducedPersonCount: 0,
reducedDays: 0,
reducedAccommodationStandard: 0,
reducedRoundTripFee: 0,
reducedAccommodationFee: 0,
reducedSubsidy: 0,
reducedSubtotal: 0,
costReductionPlan: ''
}
])
// 选中的行
const selectedRows = ref([])
// 添加行弹窗
const showAddDialog = ref(false)
const addRowCount = ref(1)
// 格式化数字
const formatNumber = (value: number) => {
if (!value) return '0.00'
return value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
// 计算预算小计
const calculateBudgetSubtotal = (row: any) => {
const times = parseFloat(row.times) || 0
const personCount = parseFloat(row.personCount) || 0
const days = parseFloat(row.days) || 0
const accommodationStandard = parseFloat(row.accommodationStandard) || 0
const roundTripFee = parseFloat(row.roundTripFee) || 0
// 住宿费 = 人数 × 天数 × 住宿标准
row.accommodationFee = personCount * days * accommodationStandard
// 补贴 = 50 × 人数 × 天数
row.subsidy = 50 * personCount * days
// 小计 = (往返路费 + 住宿费 + 补贴) / 10000
row.budgetSubtotal = (roundTripFee + row.accommodationFee + row.subsidy) / 10000 // 转换为万元
}
// 计算降成本后小计
const calculateReducedSubtotal = (row: any) => {
const times = parseFloat(row.reducedTimes) || 0
const personCount = parseFloat(row.reducedPersonCount) || 0
const days = parseFloat(row.reducedDays) || 0
const accommodationStandard = parseFloat(row.reducedAccommodationStandard) || 0
const roundTripFee = parseFloat(row.reducedRoundTripFee) || 0
// 住宿费 = 人数 × 天数 × 住宿标准
row.reducedAccommodationFee = personCount * days * accommodationStandard
// 补贴 = 50 × 人数 × 天数
row.reducedSubsidy = 50 * personCount * days
// 小计 = (往返路费 + 住宿费 + 补贴) / 10000
row.reducedSubtotal = (roundTripFee + row.reducedAccommodationFee + row.reducedSubsidy) / 10000 // 转换为万元
}
// 选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 添加行
const handleAddRows = () => {
const currentMaxSerial = Math.max(...travelData.value.map(item => item.serialNumber))
for (let i = 0; i < addRowCount.value; i++) {
travelData.value.push({
serialNumber: currentMaxSerial + i + 1,
destination: '',
purpose: '',
times: 0,
personCount: 0,
days: 0,
accommodationStandard: 0,
roundTripFee: 0,
accommodationFee: 0,
subsidy: 0,
budgetSubtotal: 0,
reducedSerialNumber: currentMaxSerial + i + 1,
reducedDestination: '',
reducedPurpose: '',
reducedTimes: 0,
reducedPersonCount: 0,
reducedDays: 0,
reducedAccommodationStandard: 0,
reducedRoundTripFee: 0,
reducedAccommodationFee: 0,
reducedSubsidy: 0,
reducedSubtotal: 0,
costReductionPlan: ''
})
}
showAddDialog.value = false
addRowCount.value = 1
}
// 删除单行
const handleDelete = (index: number) => {
travelData.value.splice(index, 1)
// 重新编号
travelData.value.forEach((item, index) => {
item.serialNumber = index + 1
item.reducedSerialNumber = index + 1
})
}
// 批量删除
const handleBatchDelete = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请选择要删除的行')
return
}
travelData.value = travelData.value.filter(item => !selectedRows.value.includes(item))
// 重新编号
travelData.value.forEach((item, index) => {
item.serialNumber = index + 1
item.reducedSerialNumber = index + 1
})
selectedRows.value = []
}
// 合计方法
const getSummaries = (param: any) => {
const { columns, data } = param
const sums: any[] = []
columns.forEach((column: any, index: number) => {
if (index === 0) {
sums[index] = '合计'
return
}
if (column.property === 'roundTripFee' || column.property === 'accommodationFee' ||
column.property === 'subsidy' || column.property === 'budgetSubtotal' ||
column.property === 'reducedRoundTripFee' || column.property === 'reducedAccommodationFee' ||
column.property === 'reducedSubsidy' || column.property === 'reducedSubtotal') {
const values = data.map((item: any) => Number(item[column.property]))
if (!values.every((value: number) => Number.isNaN(value))) {
sums[index] = values.reduce((prev: number, curr: number) => {
const value = Number(curr)
if (!Number.isNaN(value)) {
return prev + curr
} else {
return prev
}
}, 0)
sums[index] = formatNumber(sums[index])
} else {
sums[index] = ''
}
} else {
sums[index] = ''
}
})
return sums
}
// 获取总金额(供父组件调用)
const getTotalAmount = () => {
const totalBudgetAmount = travelData.value.reduce((sum, item) => sum + item.budgetSubtotal, 0)
const totalReducedAmount = travelData.value.reduce((sum, item) => sum + item.reducedSubtotal, 0)
return {
budgetAmount: totalBudgetAmount,
reducedAmount: totalReducedAmount
}
}
// 暴露方法给父组件
defineExpose({
getTotalAmount,
travelData
})
</script>
<style scoped>
.travel-cost {
padding: 20px;
}
.title {
text-align: center;
font-size: 16px;
font-weight: bold;
margin-bottom: 20px;
}
.actions {
margin-bottom: 20px;
}
.actions .el-button {
margin-right: 10px;
}
</style>