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.

505 lines
16 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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