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