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.

990 lines
29 KiB
Vue

<template>
<div class="travel-meeting-container">
<!-- 差旅费预算明细表 -->
<el-card class="mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium">差旅费预算明细表</h3>
<div class="flex gap-2 items-center">
<span class="mr-2">行数:</span>
<el-input-number
v-model="travelAddRowCount"
:min="1"
:max="10"
:step="1"
size="small"
style="width: 100px"
/>
<el-button type="primary" @click="confirmTravelAdd" size="small">
<el-icon><Plus /></el-icon>
添加
</el-button>
<el-button @click="deleteSelectedTravel" size="small" :disabled="selectedTravelRows.length === 0">
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="travelList"
style="width: 100%"
border
@selection-change="handleTravelSelectionChange"
show-summary
:summary-method="() => {
return [
'合计', '', '', '', '', '', '', '',
travelTransportTotal.toString(),
travelAccommodationTotal.toString(),
travelSubsidyTotal.toString(),
travelSubtotalTotal.toFixed(2), ''
]
}"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="序号" type="index" width="80" />
<el-table-column prop="location" label="出差地点" width="150">
<template #default="scope">
<el-input
v-model="scope.row.location"
placeholder="请输入出差地点"
size="small"
/>
</template>
</el-table-column>
<el-table-column prop="reason" label="事由" width="200">
<template #default="scope">
<el-input
v-model="scope.row.reason"
type="textarea"
placeholder="请输入事由"
size="small"
:rows="2"
resize="vertical"
/>
</template>
</el-table-column>
<el-table-column prop="times" label="次数" width="100">
<template #default="scope">
<el-input-number
v-model="scope.row.times"
:min="1"
:step="1"
size="small"
@change="calculateTravelSubtotal"
/>
</template>
</el-table-column>
<el-table-column prop="personCount" label="人数" width="100">
<template #default="scope">
<el-input-number
v-model="scope.row.personCount"
:min="1"
:step="1"
size="small"
@change="calculateTravelSubtotal"
/>
</template>
</el-table-column>
<el-table-column prop="days" label="天数" width="100">
<template #default="scope">
<el-input-number
v-model="scope.row.days"
:min="1"
:step="1"
size="small"
@change="calculateTravelSubtotal"
/>
</template>
</el-table-column>
<el-table-column prop="accommodationStandard" label="住宿标准(元)" width="150">
<template #default="scope">
<el-input-number
v-model="scope.row.accommodationStandard"
:min="0"
:step="50"
size="small"
@change="calculateTravelSubtotal"
/>
</template>
</el-table-column>
<el-table-column prop="transportFee" label="往返路费(元)" width="150">
<template #default="scope">
<el-input-number
v-model="scope.row.transportFee"
:min="0"
:step="100"
size="small"
@change="calculateTravelSubtotal"
/>
</template>
</el-table-column>
<el-table-column prop="accommodationFee" label="住宿费(元)" width="120">
<template #default="scope">
<el-input
v-model.number="scope.row.accommodationFee"
disabled
size="small"
type="number"
/>
</template>
</el-table-column>
<el-table-column prop="subsidy" label="补贴(元)" width="100">
<template #default="scope">
<el-input
v-model.number="scope.row.subsidy"
disabled
size="small"
type="number"
/>
</template>
</el-table-column>
<el-table-column prop="subtotal" label="小计(万元)" width="120">
<template #default="scope">
<el-input
v-model.number="scope.row.subtotal"
disabled
size="small"
type="number"
:precision="2"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button
type="danger"
size="small"
@click="deleteTravelRow(scope.row)"
icon="Delete"
/>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 会议费预算明细表 -->
<el-card class="mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium">会议费预算明细表</h3>
<div class="flex gap-2 items-center">
<span class="mr-2">行数:</span>
<el-input-number
v-model="meetingAddRowCount"
:min="1"
:max="10"
:step="1"
size="small"
style="width: 100px"
/>
<el-button type="primary" @click="confirmMeetingAdd" size="small">
<el-icon><Plus /></el-icon>
添加
</el-button>
<el-button @click="deleteSelectedMeeting" size="small" :disabled="selectedMeetingRows.length === 0">
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="meetingList"
style="width: 100%"
border
@selection-change="handleMeetingSelectionChange"
show-summary
:summary-method="() => {
const cols = ['合计', '', '', '', '', '', '', '', '', '', '', '']
cols[cols.length - 2] = meetingFeeTotal.toFixed(2)
return cols
}"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="序号" type="index" width="80" />
<el-table-column label="会议类型" width="150">
<template #default="scope">
<el-select
v-model="scope.row.meetingType"
placeholder="请选择会议类型"
size="small"
@change="calculateMeetingFee"
>
<el-option label="公司组织会议" value="company" />
<el-option label="外出参加会议" value="external" />
</el-select>
</template>
</el-table-column>
<!-- 组合列公司组织会议 -->
<el-table-column prop="content" label="会议内容" width="200">
<template #default="scope">
<el-input
v-model="scope.row.content"
placeholder="请输入会议内容"
size="small"
/>
</template>
</el-table-column>
<el-table-column prop="venueDailyRent" label="场地日租金(元)" width="150" v-if="showCompanyMeetingColumns">
<template #default="scope">
<el-input-number
v-model="scope.row.venueDailyRent"
:min="0"
:step="100"
size="small"
@change="calculateMeetingFee"
/>
</template>
</el-table-column>
<el-table-column prop="dailyMiscFee" label="日均杂费(元)" width="150" v-if="showCompanyMeetingColumns">
<template #default="scope">
<el-input-number
v-model="scope.row.dailyMiscFee"
:min="0"
:step="50"
size="small"
@change="calculateMeetingFee"
/>
</template>
</el-table-column>
<el-table-column prop="meetingDays" label="天数" width="100">
<template #default="scope">
<el-input-number
v-model="scope.row.meetingDays"
:min="1"
:step="1"
size="small"
@change="calculateMeetingFee"
/>
</template>
</el-table-column>
<el-table-column prop="expertTransportAccommodation" label="专家交通住宿费" width="180" v-if="showCompanyMeetingColumns">
<template #default="scope">
<el-input-number
v-model="scope.row.expertTransportAccommodation"
:min="0"
:step="100"
size="small"
@change="calculateMeetingFee"
/>
</template>
</el-table-column>
<!-- 组合列外出参加会议 -->
<el-table-column prop="participantCount" label="人数" width="100" v-if="showExternalMeetingColumns">
<template #default="scope">
<el-input-number
v-model="scope.row.participantCount"
:min="1"
:step="1"
size="small"
@change="calculateMeetingFee"
/>
</template>
</el-table-column>
<el-table-column prop="feePerPerson" label="人均交费" width="120" v-if="showExternalMeetingColumns">
<template #default="scope">
<el-input-number
v-model="scope.row.feePerPerson"
:min="0"
:step="100"
size="small"
@change="calculateMeetingFee"
/>
</template>
</el-table-column>
<el-table-column prop="meetingFee" label="会议费(万元)" width="120">
<template #default="scope">
<el-input
v-model.number="scope.row.meetingFee"
disabled
size="small"
type="number"
:precision="2"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button
type="danger"
size="small"
@click="deleteMeetingRow(scope.row)"
icon="Delete"
/>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 国际交流费预算明细表 -->
<el-card>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium">国际交流费预算明细表</h3>
<div class="flex gap-2 items-center">
<span class="mr-2">行数:</span>
<el-input-number
v-model="internationalAddRowCount"
:min="1"
:max="10"
:step="1"
size="small"
style="width: 100px"
/>
<el-button type="primary" @click="confirmInternationalAdd" size="small">
<el-icon><Plus /></el-icon>
添加
</el-button>
<el-button @click="deleteSelectedInternational" size="small" :disabled="selectedInternationalRows.length === 0">
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="internationalList"
style="width: 100%"
border
@selection-change="handleInternationalSelectionChange"
show-summary
:summary-method="() => {
const cols = ['合计', '', '', '', '', '', '', '', '', '']
cols[cols.length - 2] = internationalSubtotalTotal.toFixed(2)
return cols
}"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="序号" type="index" width="80" />
<el-table-column prop="exchangeType" label="合作交流类型" width="180">
<template #default="scope">
<el-select
v-model="scope.row.exchangeType"
placeholder="请选择交流类型"
size="small"
filterable
allow-create
default-first-option
>
<el-option label="学术访问" value="学术访问" />
<el-option label="技术培训" value="技术培训" />
<el-option label="国际会议" value="国际会议" />
<el-option label="合作研发" value="合作研发" />
<el-option label="出国考察" value="出国考察" />
</el-select>
</template>
</el-table-column>
<el-table-column prop="country" label="国家和地区" width="120">
<template #default="scope">
<el-input
v-model="scope.row.country"
placeholder="请输入国家和地区"
size="small"
/>
</template>
</el-table-column>
<el-table-column prop="institution" label="机构" width="150">
<template #default="scope">
<el-input
v-model="scope.row.institution"
placeholder="请输入机构"
size="small"
/>
</template>
</el-table-column>
<el-table-column prop="personCount" label="人数(人)" width="120">
<template #default="scope">
<el-input-number
v-model="scope.row.personCount"
:min="1"
:step="1"
size="small"
@change="calculateInternationalSubtotal"
/>
</template>
</el-table-column>
<el-table-column prop="days" label="时间(天)" width="120">
<template #default="scope">
<el-input-number
v-model="scope.row.days"
:min="1"
:step="1"
size="small"
@change="calculateInternationalSubtotal"
/>
</template>
</el-table-column>
<el-table-column prop="transportAccommodationFee" label="往返路费及住宿费" width="180">
<template #default="scope">
<el-input-number
v-model="scope.row.transportAccommodationFee"
:min="0"
:step="100"
size="small"
@change="calculateInternationalSubtotal"
/>
</template>
</el-table-column>
<el-table-column prop="subsidy" label="补贴(元)" width="120">
<template #default="scope">
<el-input
v-model.number="scope.row.subsidy"
disabled
size="small"
type="number"
/>
</template>
</el-table-column>
<el-table-column prop="subtotal" label="小计(万元)" width="120">
<template #default="scope">
<el-input
v-model.number="scope.row.subtotal"
disabled
size="small"
type="number"
:precision="2"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button
type="danger"
size="small"
@click="deleteInternationalRow(scope.row)"
icon="Delete"
/>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
// 定义差旅费数据类型
interface TravelItem {
id?: string
location: string
reason: string
times: number
personCount: number
days: number
accommodationStandard: number
transportFee: number
accommodationFee: number
subsidy: number
subtotal: number
}
// 定义会议费数据类型
interface MeetingItem {
id?: string
meetingType: string
content: string
venueDailyRent: number
dailyMiscFee: number
meetingDays: number
expertTransportAccommodation: number
participantCount: number
feePerPerson: number
meetingFee: number
}
// 定义国际交流费数据类型
interface InternationalItem {
id?: string
exchangeType: string
country: string
institution: string
personCount: number
days: number
transportAccommodationFee: number
subsidy: number
subtotal: number
}
// Props
const props = defineProps<{
projectId?: string
}>()
// Emits
const emit = defineEmits<{
update: [data: {
travelTotal: number,
meetingTotal: number,
internationalTotal: number
}]
}>()
// 响应式数据
const loading = ref(false)
// 差旅费相关
const travelList = ref<TravelItem[]>([])
const selectedTravelRows = ref<TravelItem[]>([])
const travelAddRowCount = ref(1)
// 会议费相关
const meetingList = ref<MeetingItem[]>([])
const selectedMeetingRows = ref<MeetingItem[]>([])
const meetingAddRowCount = ref(1)
const showCompanyMeetingColumns = ref(true)
const showExternalMeetingColumns = ref(true)
// 国际交流费相关
const internationalList = ref<InternationalItem[]>([])
const selectedInternationalRows = ref<InternationalItem[]>([])
const internationalAddRowCount = ref(1)
// 计算差旅费合计
const travelTransportTotal = computed(() => {
return travelList.value.reduce((sum, item) => sum + (item.transportFee || 0), 0)
})
const travelAccommodationTotal = computed(() => {
return travelList.value.reduce((sum, item) => sum + (item.accommodationFee || 0), 0)
})
const travelSubsidyTotal = computed(() => {
return travelList.value.reduce((sum, item) => sum + (item.subsidy || 0), 0)
})
const travelSubtotalTotal = computed(() => {
return travelList.value.reduce((sum, item) => sum + (item.subtotal || 0), 0)
})
// 计算会议费合计
const meetingFeeTotal = computed(() => {
return meetingList.value.reduce((sum, item) => sum + (item.meetingFee || 0), 0)
})
// 计算国际交流费合计
const internationalSubtotalTotal = computed(() => {
return internationalList.value.reduce((sum, item) => sum + (item.subtotal || 0), 0)
})
// 计算差旅费小计
const calculateTravelSubtotal = () => {
travelList.value.forEach(item => {
// 住宿费 = 人数 * 天数 * 住宿标准
item.accommodationFee = (item.personCount || 0) * (item.days || 0) * (item.accommodationStandard || 0)
// 补贴 = 50 * 人数 * 天数
item.subsidy = 50 * (item.personCount || 0) * (item.days || 0)
// 小计 = (往返路费 + 住宿费 + 补贴) / 10000
item.subtotal = ((item.transportFee || 0) + item.accommodationFee + item.subsidy) / 10000
item.subtotal = Math.round(item.subtotal * 100) / 100 // 保留两位小数
})
emitUpdate()
}
// 计算会议费
const calculateMeetingFee = () => {
meetingList.value.forEach(item => {
if (item.meetingType === 'company') {
// 公司组织会议:((场地日租金 + 日均杂费) * 天数 + 专家交通住宿费) / 10000
item.meetingFee = (((item.venueDailyRent || 0) + (item.dailyMiscFee || 0)) * (item.meetingDays || 0) +
(item.expertTransportAccommodation || 0)) / 10000
} else if (item.meetingType === 'external') {
// 外出参加会议:(人数 * 人均交费) / 10000
item.meetingFee = ((item.participantCount || 0) * (item.feePerPerson || 0)) / 10000
}
item.meetingFee = Math.round(item.meetingFee * 100) / 100 // 保留两位小数
})
emitUpdate()
}
// 计算国际交流费小计
const calculateInternationalSubtotal = () => {
internationalList.value.forEach(item => {
// 如果合作交流类型是出国考察,则补贴=40*人数*时间
if (item.exchangeType === '出国考察') {
item.subsidy = 40 * (item.personCount || 0) * (item.days || 0)
} else if (!item.subsidy) {
// 其他类型如果没有设置补贴默认设为0
item.subsidy = 0
}
// 小计 = (往返路费及住宿费 + 补贴) / 10000
item.subtotal = ((item.transportAccommodationFee || 0) + (item.subsidy || 0)) / 10000
item.subtotal = Math.round(item.subtotal * 100) / 100 // 保留两位小数
})
emitUpdate()
}
// 发出更新事件
const emitUpdate = () => {
emit('update', {
travelCost: travelSubtotalTotal.value,
meetingCost: meetingFeeTotal.value,
internationalExchangeCost: internationalSubtotalTotal.value
})
}
// 差旅费相关方法
const confirmTravelAdd = () => {
for (let i = 0; i < travelAddRowCount.value; i++) {
const newItem: TravelItem = {
location: '',
reason: '',
times: 1,
personCount: 1,
days: 1,
accommodationStandard: 0,
transportFee: 0,
accommodationFee: 0,
subsidy: 0,
subtotal: 0
}
travelList.value.push(newItem)
}
// 滚动到最下方并聚焦到新添加的行
setTimeout(() => {
const table = document.querySelector('.el-table__body-wrapper')
if (table) {
table.scrollTop = table.scrollHeight
}
// 尝试聚焦到新添加的第一行的第一个输入框
const rows = document.querySelectorAll('.el-table__body tr')
const newRow = rows[rows.length - travelAddRowCount.value]
if (newRow) {
const firstInput = newRow.querySelector('input')
if (firstInput) {
firstInput.focus()
}
}
}, 0)
ElMessage.success(`成功添加 ${travelAddRowCount.value}`)
}
const deleteTravelRow = (row: TravelItem) => {
const index = travelList.value.findIndex(item => item === row)
if (index !== -1) {
travelList.value.splice(index, 1)
calculateTravelSubtotal()
ElMessage.success('删除成功')
}
}
const deleteSelectedTravel = () => {
if (selectedTravelRows.value.length === 0) {
ElMessage.warning('请先选择要删除的行')
return
}
selectedTravelRows.value.forEach(row => {
const index = travelList.value.findIndex(item => item === row)
if (index !== -1) {
travelList.value.splice(index, 1)
}
})
selectedTravelRows.value = []
calculateTravelSubtotal()
ElMessage.success('删除成功')
}
const handleTravelSelectionChange = (selection: TravelItem[]) => {
selectedTravelRows.value = selection
}
// 会议费相关方法
const confirmMeetingAdd = () => {
for (let i = 0; i < meetingAddRowCount.value; i++) {
const newItem: MeetingItem = {
meetingType: 'company',
content: '',
venueDailyRent: 0,
dailyMiscFee: 0,
meetingDays: 1,
expertTransportAccommodation: 0,
participantCount: 1,
feePerPerson: 0,
meetingFee: 0
}
meetingList.value.push(newItem)
}
// 滚动到最下方并聚焦到新添加的行
setTimeout(() => {
const table = document.querySelector('.el-table__body-wrapper')
if (table) {
table.scrollTop = table.scrollHeight
}
// 尝试聚焦到新添加的第一行的第一个输入框
const rows = document.querySelectorAll('.el-table__body tr')
const newRow = rows[rows.length - meetingAddRowCount.value]
if (newRow) {
const firstInput = newRow.querySelector('input, select')
if (firstInput) {
firstInput.focus()
}
}
}, 0)
ElMessage.success(`成功添加 ${meetingAddRowCount.value}`)
}
const deleteMeetingRow = (row: MeetingItem) => {
const index = meetingList.value.findIndex(item => item === row)
if (index !== -1) {
meetingList.value.splice(index, 1)
calculateMeetingFee()
ElMessage.success('删除成功')
}
}
const deleteSelectedMeeting = () => {
if (selectedMeetingRows.value.length === 0) {
ElMessage.warning('请先选择要删除的行')
return
}
selectedMeetingRows.value.forEach(row => {
const index = meetingList.value.findIndex(item => item === row)
if (index !== -1) {
meetingList.value.splice(index, 1)
}
})
selectedMeetingRows.value = []
calculateMeetingFee()
ElMessage.success('删除成功')
}
const handleMeetingSelectionChange = (selection: MeetingItem[]) => {
selectedMeetingRows.value = selection
}
// 国际交流费相关方法
const confirmInternationalAdd = () => {
for (let i = 0; i < internationalAddRowCount.value; i++) {
const newItem: InternationalItem = {
exchangeType: '',
country: '',
institution: '',
personCount: 1,
days: 1,
transportAccommodationFee: 0,
subsidy: 0,
subtotal: 0
}
internationalList.value.push(newItem)
}
// 滚动到最下方并聚焦到新添加的行
setTimeout(() => {
const table = document.querySelector('.el-table__body-wrapper')
if (table) {
table.scrollTop = table.scrollHeight
}
// 尝试聚焦到新添加的第一行的第一个输入框
const rows = document.querySelectorAll('.el-table__body tr')
const newRow = rows[rows.length - internationalAddRowCount.value]
if (newRow) {
const firstInput = newRow.querySelector('input, select')
if (firstInput) {
firstInput.focus()
}
}
}, 0)
// 新增行后立即计算一次,确保补贴正确
calculateInternationalSubtotal()
ElMessage.success(`成功添加 ${internationalAddRowCount.value}`)
}
const deleteInternationalRow = (row: InternationalItem) => {
const index = internationalList.value.findIndex(item => item === row)
if (index !== -1) {
internationalList.value.splice(index, 1)
calculateInternationalSubtotal()
ElMessage.success('删除成功')
}
}
const deleteSelectedInternational = () => {
if (selectedInternationalRows.value.length === 0) {
ElMessage.warning('请先选择要删除的行')
return
}
selectedInternationalRows.value.forEach(row => {
const index = internationalList.value.findIndex(item => item === row)
if (index !== -1) {
internationalList.value.splice(index, 1)
}
})
selectedInternationalRows.value = []
calculateInternationalSubtotal()
ElMessage.success('删除成功')
}
const handleInternationalSelectionChange = (selection: InternationalItem[]) => {
selectedInternationalRows.value = selection
}
// 监听projectId变化重新加载数据
watch(() => props.projectId, (newProjectId) => {
if (newProjectId) {
loadAllData(newProjectId)
}
}, { immediate: true })
// 加载所有数据
const loadAllData = async (projectId: string) => {
loading.value = true
try {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500))
// 模拟差旅费数据
travelList.value = [
{
id: '1',
location: '北京',
reason: '项目启动会议',
times: 1,
personCount: 3,
days: 2,
accommodationStandard: 400,
transportFee: 1200,
accommodationFee: 2400,
subsidy: 300,
subtotal: 0.39
}
]
// 模拟会议费数据
meetingList.value = [
{
id: '1',
meetingType: 'company',
content: '技术研讨会',
venueDailyRent: 5000,
dailyMiscFee: 1000,
meetingDays: 2,
expertTransportAccommodation: 10000,
participantCount: 0,
feePerPerson: 0,
meetingFee: 2.2
}
]
// 模拟国际交流费数据
internationalList.value = [
{
id: '1',
exchangeType: 'conference',
country: '美国',
institution: 'IEEE',
personCount: 2,
days: 7,
transportAccommodationFee: 50000,
subsidy: 5000,
subtotal: 5.5
}
]
// 计算各项费用
calculateTravelSubtotal()
calculateMeetingFee()
calculateInternationalSubtotal()
} catch (error) {
ElMessage.error('加载数据失败')
console.error('加载数据失败:', error)
} finally {
loading.value = false
}
}
// 暴露方法供父组件调用
defineExpose({
travelList,
meetingList,
internationalList,
travelSubtotalTotal,
meetingFeeTotal,
internationalSubtotalTotal,
calculateTravelSubtotal,
calculateMeetingFee,
calculateInternationalSubtotal,
loadAllData
})
</script>
<style scoped>
.travel-meeting-container {
padding: 0;
}
:deep(.el-table) {
margin-bottom: 0;
}
:deep(.el-table__footer-wrapper) {
background-color: #f5f7fa;
}
:deep(.el-table__footer-wrapper .el-table__footer) {
padding: 12px 0;
border-top: 1px solid #ebeef5;
}
:deep(.el-table__footer-wrapper td) {
font-weight: 600;
}
:deep(.el-input__wrapper) {
box-shadow: none;
}
:deep(.el-input-number) {
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.bg-gray-50 {
background-color: #f5f7fa;
}
.p-3 {
padding: 12px;
}
.rounded {
border-radius: 4px;
}
</style>