|
|
<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" :precision="0" size="small" style="width: 100px" />
|
|
|
<el-button type="primary" @click="confirmTravelAdd" size="small">
|
|
|
<el-icon>
|
|
|
<Plus />
|
|
|
</el-icon>
|
|
|
添加
|
|
|
</el-button>
|
|
|
<el-button type="danger" @click="handleTravelBatchDelete" 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 [
|
|
|
'',
|
|
|
'',
|
|
|
'',
|
|
|
'',
|
|
|
'',
|
|
|
'',
|
|
|
'',
|
|
|
'合计',
|
|
|
formatNumber(travelTransportTotal),
|
|
|
formatNumber(travelAccommodationTotal),
|
|
|
formatNumber(travelSubsidyTotal),
|
|
|
formatNumber(travelSubtotalTotal),
|
|
|
''
|
|
|
];
|
|
|
}
|
|
|
"
|
|
|
>
|
|
|
<el-table-column type="selection" width="55" />
|
|
|
<el-table-column prop="sortOrder" label="序号" type="index" width="80" />
|
|
|
<el-table-column prop="tripLocation" label="出差地点" width="150">
|
|
|
<template #default="scope">
|
|
|
<el-input v-model="scope.row.tripLocation" placeholder="请输入出差地点" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="reason" label="事由" min-width="160">
|
|
|
<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="frequency" label="次数" width="130">
|
|
|
<template #default="scope">
|
|
|
<el-input-number v-model="scope.row.frequency" :min="0" :step="1" :precision="0" size="small" @change="calculateTravelSubtotal" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="peopleNumber" label="人数" width="130">
|
|
|
<template #default="scope">
|
|
|
<el-input-number v-model="scope.row.peopleNumber" :min="0" :precision="0" :step="1" size="small" @change="calculateTravelSubtotal" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="days" label="天数" width="130">
|
|
|
<template #default="scope">
|
|
|
<el-input-number v-model="scope.row.days" :min="0" :precision="0" :step="1" size="small" @change="calculateTravelSubtotal" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="stayStandard" label="住宿标准(元)" width="150">
|
|
|
<template #default="scope">
|
|
|
<el-input-number v-model="scope.row.stayStandard" :min="0" :precision="2" :step="10" size="small" @change="calculateTravelSubtotal" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="travelExpenses" label="往返路费(元)" width="150">
|
|
|
<template #default="scope">
|
|
|
<el-input-number v-model="scope.row.travelExpenses" :min="0" :precision="2" :step="10" size="small" @change="calculateTravelSubtotal" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="stayCosts" label="住宿费(元)" width="120">
|
|
|
<template #default="scope">
|
|
|
{{ formatNumber(scope.row.stayCosts) }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="subsidyCosts" label="补贴(元)" width="100">
|
|
|
<template #default="scope">
|
|
|
{{ formatNumber(scope.row.subsidyCosts) }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="subtotalCosts" label="小计(万元)" width="120">
|
|
|
<template #default="scope">
|
|
|
{{ format2TenThousandNumber(scope.row.subtotalCosts) }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="操作" width="120" fixed="right">
|
|
|
<template #default="scope">
|
|
|
<el-button type="danger" size="small" @click="handleTravelDelete(scope.$index, scope.row)" icon="Delete" >
|
|
|
删除
|
|
|
</el-button>
|
|
|
</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="transportationAddRowCount" :min="1" :max="10" :step="1" size="small" style="width: 100px" />
|
|
|
<el-button type="primary" @click="confirmTransportationAdd" size="small">
|
|
|
<el-icon>
|
|
|
<Plus />
|
|
|
</el-icon>
|
|
|
添加
|
|
|
</el-button>
|
|
|
<el-button type="danger" @click="handleTransportationBatchDelete" size="small" :disabled="selectedTransportationRows.length === 0">
|
|
|
<el-icon>
|
|
|
<Delete />
|
|
|
</el-icon>
|
|
|
删除
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<el-table
|
|
|
v-loading="loading"
|
|
|
:data="transportationList"
|
|
|
style="width: 100%"
|
|
|
border
|
|
|
@selection-change="handleTransportationSelectionChange"
|
|
|
show-summary
|
|
|
:summary-method="
|
|
|
() => {
|
|
|
const cols = ['', '', '', '合计(万元)', '',''];
|
|
|
cols[cols.length - 1] = formatNumber(transportationSubtotalTotal);
|
|
|
return cols;
|
|
|
}
|
|
|
"
|
|
|
>
|
|
|
<el-table-column type="selection" width="55" />
|
|
|
<el-table-column prop="sortOrder" label="序号" type="index" width="80" />
|
|
|
<el-table-column prop="tripLocation" label="出行起止地点" width="200">
|
|
|
<template #default="scope">
|
|
|
<el-input v-model="scope.row.tripLocation" placeholder="请输入出行起止地点" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="reason" label="出行任务" min-width="180">
|
|
|
<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="subtotalCosts" label="金额(元)" width="180">
|
|
|
<template #default="scope">
|
|
|
<el-input-number
|
|
|
v-model="scope.row.subtotalCosts"
|
|
|
:min="0"
|
|
|
:precision="2"
|
|
|
:step="10"
|
|
|
size="small"
|
|
|
/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="price" label="小计(万元)" width="120">
|
|
|
<template #default="scope">
|
|
|
{{ format2TenThousandNumber(scope.row.subtotalCosts) }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="操作" width="120" fixed="right">
|
|
|
<template #default="scope">
|
|
|
<el-button type="danger" size="small" @click="handleTransportationDelete(scope.$index, scope.row)" icon="Delete">
|
|
|
删除</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</el-card>
|
|
|
|
|
|
<div class="mt-4 bg-gray-50 p-3 rounded text-left font-semibold">
|
|
|
<p>注:开展实验(试验)、考察、业务调研、学术交流等发生的外埠差旅费、市内交通费用等。开支标准按照公司有关规定执行。</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import { ref, computed, watch } from 'vue';
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
import { rdBudgetTravelCostVO } from '@/api/oa/erp/budgetInfo/rd/rdBudgetTravelCost/types';
|
|
|
|
|
|
// Props
|
|
|
const props = defineProps<{
|
|
|
projectId?: string;
|
|
|
}>();
|
|
|
|
|
|
const TRIP_TYPE = {
|
|
|
TRAVEL: '1', //差旅费
|
|
|
TRANSPORTATION: '2' //交通费
|
|
|
};
|
|
|
|
|
|
// 响应式数据
|
|
|
const loading = ref(false);
|
|
|
|
|
|
// 差旅费相关
|
|
|
const travelList = ref<rdBudgetTravelCostVO[]>([]);
|
|
|
const selectedTravelRows = ref<rdBudgetTravelCostVO[]>([]);
|
|
|
const travelAddRowCount = ref(1);
|
|
|
const toDeletedTravelCostIdList = ref([]);
|
|
|
|
|
|
// 交通费相关
|
|
|
const transportationList = ref<rdBudgetTravelCostVO[]>([]);
|
|
|
const selectedTransportationRows = ref<rdBudgetTravelCostVO[]>([]);
|
|
|
const transportationAddRowCount = ref(1);
|
|
|
const toDeletedTransportationCostIdList = ref([]);
|
|
|
|
|
|
// 计算差旅费合计
|
|
|
//往返路费
|
|
|
const travelTransportTotal = computed(() => {
|
|
|
return travelList.value.reduce((sum, item) => sum + (Number(item.travelExpenses) || 0), 0);
|
|
|
});
|
|
|
|
|
|
//住宿费
|
|
|
const travelAccommodationTotal = computed(() => {
|
|
|
return travelList.value.reduce((sum, item) => sum + (Number(item.stayCosts) || 0), 0);
|
|
|
});
|
|
|
|
|
|
//补贴
|
|
|
const travelSubsidyTotal = computed(() => {
|
|
|
return travelList.value.reduce((sum, item) => sum + (Number(item.subsidyCosts) || 0), 0);
|
|
|
});
|
|
|
|
|
|
//小计(需要转成万元,2位小数再相加)
|
|
|
const travelSubtotalTotal = computed(() => {
|
|
|
return travelList.value.reduce((sum, item) => {
|
|
|
const amountInTenThousand = (Number(item.subtotalCosts) || 0) / 10000;
|
|
|
const formattedAmount = parseFloat(amountInTenThousand.toFixed(2));
|
|
|
return sum + formattedAmount;
|
|
|
}, 0);
|
|
|
});
|
|
|
|
|
|
// 计算交通费合计(需要转成万元,2位小数再相加)
|
|
|
const transportationSubtotalTotal = computed(() => {
|
|
|
return transportationList.value.reduce((sum, item) => {
|
|
|
const amountInTenThousand = (Number(item.subtotalCosts) || 0) / 10000;
|
|
|
const formattedAmount = parseFloat(amountInTenThousand.toFixed(2));
|
|
|
return sum + formattedAmount;
|
|
|
}, 0);
|
|
|
});
|
|
|
|
|
|
// 格式化数字,元转换为万元
|
|
|
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 calculateTravelSubtotal = () => {
|
|
|
travelList.value.forEach((item) => {
|
|
|
// 住宿费 = 人数 * 天数 * 住宿标准
|
|
|
item.stayCosts = (item.peopleNumber || 0) * (item.frequency || 0) * (item.days || 0) * (item.stayStandard || 0);
|
|
|
// 补贴 = 90 * 人数 * 天数
|
|
|
item.subsidyCosts = 90 * (item.peopleNumber || 0) * (item.frequency || 0) * (item.days || 0);
|
|
|
// 小计 = (往返路费 + 住宿费 + 补贴) / 10000
|
|
|
item.subtotalCosts = (item.travelExpenses || 0) + item.stayCosts + item.subsidyCosts;
|
|
|
});
|
|
|
};
|
|
|
|
|
|
|
|
|
const getTravelAmount = () => {
|
|
|
return {
|
|
|
travelSubtotalTotal: (Number(travelSubtotalTotal.value) || 0) * 10000,
|
|
|
transportationSubtotalTotal: (Number(transportationSubtotalTotal.value) || 0) * 10000
|
|
|
};
|
|
|
};
|
|
|
|
|
|
// 差旅费相关方法
|
|
|
const confirmTravelAdd = () => {
|
|
|
const currentSortOrder = travelList.value && travelList.value.length > 0 ? Math.max(...travelList.value.map((item) => item.sortOrder)) : 0;
|
|
|
for (let i = 0; i < travelAddRowCount.value; i++) {
|
|
|
const newItem: rdBudgetTravelCostVO = {
|
|
|
sortOrder: currentSortOrder + i + 1,
|
|
|
tripType: TRIP_TYPE.TRAVEL,
|
|
|
tripLocation: '',
|
|
|
reason: '',
|
|
|
frequency: undefined,
|
|
|
peopleNumber: undefined,
|
|
|
days: undefined,
|
|
|
stayStandard: 180,
|
|
|
travelExpenses: undefined,
|
|
|
stayCosts: 0,
|
|
|
subsidyCosts: 0,
|
|
|
subtotalCosts: 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);
|
|
|
};
|
|
|
|
|
|
const handleTravelSelectionChange = (selection: rdBudgetTravelCostVO[]) => {
|
|
|
selectedTravelRows.value = selection;
|
|
|
};
|
|
|
|
|
|
// 删除单行
|
|
|
const handleTravelDelete = (index: number, row: rdBudgetTravelCostVO) => {
|
|
|
travelList.value.splice(index, 1);
|
|
|
// 重新编号
|
|
|
travelList.value.forEach((item, index) => {
|
|
|
item.sortOrder = index + 1;
|
|
|
});
|
|
|
if (row.travelCostId) {
|
|
|
toDeletedTravelCostIdList.value.push(row.travelCostId);
|
|
|
}
|
|
|
calculateTravelSubtotal();
|
|
|
};
|
|
|
|
|
|
// 批量删除
|
|
|
const handleTravelBatchDelete = () => {
|
|
|
if (selectedTravelRows.value.length === 0) {
|
|
|
ElMessage.warning('请选择要删除的行');
|
|
|
return;
|
|
|
}
|
|
|
selectedTravelRows.value.forEach((selectedRow) => {
|
|
|
if (selectedRow.travelCostId) {
|
|
|
toDeletedTravelCostIdList.value.push(selectedRow.travelCostId);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
travelList.value = travelList.value.filter((item) => !selectedTravelRows.value.includes(item));
|
|
|
// 重新编号
|
|
|
travelList.value.forEach((item, index) => {
|
|
|
item.sortOrder = index + 1;
|
|
|
});
|
|
|
calculateTravelSubtotal();
|
|
|
selectedTravelRows.value = [];
|
|
|
};
|
|
|
|
|
|
|
|
|
// 交通费相关方法
|
|
|
const confirmTransportationAdd = () => {
|
|
|
const currentSortOrder = transportationList.value && transportationList.value.length > 0 ? Math.max(...transportationList.value.map((item) => item.sortOrder)) : 0;
|
|
|
|
|
|
for (let i = 0; i < transportationAddRowCount.value; i++) {
|
|
|
const newItem: rdBudgetTravelCostVO = {
|
|
|
sortOrder: currentSortOrder + i + 1,
|
|
|
tripType: TRIP_TYPE.TRANSPORTATION,
|
|
|
tripLocation: '',
|
|
|
reason: '',
|
|
|
};
|
|
|
transportationList.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 - transportationAddRowCount.value];
|
|
|
if (newRow) {
|
|
|
const firstInput = newRow.querySelector('input, select');
|
|
|
if (firstInput) {
|
|
|
firstInput.focus();
|
|
|
}
|
|
|
}
|
|
|
}, 0);
|
|
|
|
|
|
};
|
|
|
|
|
|
const handleTransportationSelectionChange = (selection: rdBudgetTravelCostVO[]) => {
|
|
|
selectedTransportationRows.value = selection;
|
|
|
};
|
|
|
|
|
|
// 删除单行
|
|
|
const handleTransportationDelete = (index: number, row: rdBudgetTravelCostVO) => {
|
|
|
transportationList.value.splice(index, 1);
|
|
|
// 重新编号
|
|
|
transportationList.value.forEach((item, index) => {
|
|
|
item.sortOrder = index + 1;
|
|
|
});
|
|
|
if (row.travelCostId) {
|
|
|
toDeletedTravelCostIdList.value.push(row.travelCostId);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 批量删除
|
|
|
const handleTransportationBatchDelete = () => {
|
|
|
if (selectedTransportationRows.value.length === 0) {
|
|
|
ElMessage.warning('请选择要删除的行');
|
|
|
return;
|
|
|
}
|
|
|
selectedTransportationRows.value.forEach((selectedRow) => {
|
|
|
if (selectedRow.travelCostId) {
|
|
|
toDeletedTravelCostIdList.value.push(selectedRow.travelCostId);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
transportationList.value = transportationList.value.filter((item) => !selectedTransportationRows.value.includes(item));
|
|
|
// 重新编号
|
|
|
transportationList.value.forEach((item, index) => {
|
|
|
item.sortOrder = index + 1;
|
|
|
});
|
|
|
selectedTransportationRows.value = [];
|
|
|
};
|
|
|
|
|
|
// 监听projectId变化,重新加载数据
|
|
|
watch(
|
|
|
() => props.projectId,
|
|
|
(newProjectId) => {
|
|
|
if (newProjectId) {
|
|
|
}
|
|
|
},
|
|
|
{ immediate: true }
|
|
|
);
|
|
|
|
|
|
// 暴露方法供父组件调用
|
|
|
defineExpose({
|
|
|
travelList,
|
|
|
toDeletedTravelCostIdList,
|
|
|
transportationList,
|
|
|
getTravelAmount
|
|
|
});
|
|
|
</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-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>
|