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.

654 lines
21 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="labor-service-container">
<!-- 技术服务费预算明细表 -->
<el-card class="mb-6">
<h3 class="text-lg font-medium mb-4">专家咨询费预算明细表</h3>
<div>
<!-- 会议形式 -->
<div class="mb-6">
<div class="flex items-center justify-between mb-3 ml-4">
<h5 class="font-medium text-sm">1. 会议形式</h5>
<div class="flex items-center gap-2">
<span class="text-sm" style="white-space: nowrap">行数:</span>
<el-input-number v-model="expertMeetingAddRowCount" :min="1" :max="100" :step="1" size="small" />
<el-button type="primary" @click="confirmExpertMeetingAdd" size="small">
<el-icon>
<Plus />
</el-icon>
添加
</el-button>
<el-button type="danger" @click="handleExpertMeetingBatchDelete" size="small" :disabled="selectedExpertMeetingRows.length === 0">
<el-icon>
<Delete />
</el-icon>
删除
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="expertMeetingList"
style="width: 100%"
:summary-method="getExpertMeetingSummaries"
show-summary
border
@selection-change="handleExpertMeetingSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="sortOrder" label="序号" width="80" align="center" />
<el-table-column prop="techContent" label="内容" min-width="200">
<template #default="scope">
<el-input v-model="scope.row.techContent" placeholder="请输入咨询内容" />
</template>
</el-table-column>
<el-table-column prop="professionalDirection" label="专业方向" width="180">
<template #default="scope">
<el-input v-model="scope.row.professionalDirection" placeholder="请输入专业方向" />
</template>
</el-table-column>
<el-table-column prop="peopleNumber" label="专家人数" width="150">
<template #default="scope">
<el-input-number
v-model="scope.row.peopleNumber"
:min="0"
:step="1"
:precision="0"
size="small"
/>
</template>
</el-table-column>
<el-table-column prop="days" label="天数" width="150">
<template #default="scope">
<el-input-number v-model="scope.row.days" :min="0" :step="1" :precision="2" size="small" />
</template>
</el-table-column>
<el-table-column prop="price" label="金额(万元)" width="160">
<template #default="scope">
<el-input-number v-model="scope.row.price" :min="0" :step="1" :precision="2" size="small" />
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button type="danger" size="small" @click="handleExpertMeetingDelete(scope.$index, scope.row)" icon="Delete"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 通讯形式 -->
<div>
<div class="flex items-center justify-between mb-3 ml-4">
<h5 class="font-medium text-sm">2. 通讯形式</h5>
<div class="flex items-center gap-2">
<span class="text-sm" style="white-space: nowrap">行数:</span>
<el-input-number v-model="expertCommAddRowCount" :min="1" :max="100" :step="1" size="small" />
<el-button type="primary" @click="confirmExpertCommAdd" size="small">
<el-icon>
<Plus />
</el-icon>
添加
</el-button>
<el-button type="danger" @click="handleExpertCommBatchDelete" size="small" :disabled="selectedExpertCommRows.length === 0">
<el-icon>
<Delete />
</el-icon>
删除
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="expertCommList"
style="width: 100%"
:summary-method="getExpertCommSummaries"
show-summary
@selection-change="handleExpertCommSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="sortOrder" label="序号" width="80" align="center" />
<el-table-column prop="techContent" label="内容" min-width="200">
<template #default="scope">
<el-input v-model="scope.row.techContent" placeholder="请输入咨询内容" />
</template>
</el-table-column>
<el-table-column prop="professionalDirection" label="专业方向" width="180">
<template #default="scope">
<el-input v-model="scope.row.professionalDirection" placeholder="请输入专业方向" />
</template>
</el-table-column>
<el-table-column prop="peopleNumber" label="专家人数" width="150">
<template #default="scope">
<el-input-number
v-model="scope.row.peopleNumber"
:min="0"
:step="1"
:precision="0"
size="small"
/>
</template>
</el-table-column>
<el-table-column prop="frequency" label="次数" width="150">
<template #default="scope">
<el-input-number v-model="scope.row.frequency" :min="0" :step="1" :precision="0" size="small" />
</template>
</el-table-column>
<el-table-column prop="price" label="金额(万元)" width="150">
<template #default="scope">
<el-input-number v-model="scope.row.price" :min="0" :step="1" :precision="2" size="small" />
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="scope">
<el-button type="danger" size="small" @click="handleExpertCommDelete(scope.$index, scope.row)" icon="Delete"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 专家咨询合计 -->
<div class="mt-2 bg-gray-50 p-3 rounded text-right font-semibold border-t border-gray-200">
专家咨询合计: {{ formatNumber(expertConsultSubtotal) }} 万元
</div>
</div>
</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="text-sm" style="white-space: nowrap">行数:</span>
<el-input-number v-model="techAddRowCount" :min="1" :max="100" :step="1" size="small" />
<el-button type="primary" @click="confirmTechAdd" size="small">
<el-icon>
<Plus />
</el-icon>
添加
</el-button>
<el-button type="danger" @click="handleTechBatchDelete" size="small" :disabled="selectedTechRows.length === 0">
<el-icon>
<Delete />
</el-icon>
删除
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="techConsultList"
:summary-method="getProductDesignSummaries"
show-summary
style="width: 100%"
border
@selection-change="handleTechSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="sortOrder" label="序号" width="80" align="center" />
<el-table-column prop="techContent" label="内容" min-width="200">
<template #default="scope">
<el-input v-model="scope.row.techContent" placeholder="请输入咨询开发内容" />
</template>
</el-table-column>
<el-table-column prop="unitName" label="单位" width="160">
<template #default="scope">
<el-input v-model="scope.row.unitName" placeholder="请输入单位" />
</template>
</el-table-column>
<el-table-column prop="price" label="金额(万元)" width="160">
<template #default="scope">
<el-input-number v-model="scope.row.price" :min="0" :step="1" :precision="2" size="small" />
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="200">
<template #default="scope">
<el-input v-model="scope.row.remark" placeholder="请输入备注" type="textarea" :rows="2" />
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button type="danger" size="small" @click="handleTechDelete(scope.$index, scope.row)" icon="Delete"> 删除 </el-button>
</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';
import { rdBudgetTechCostVO } from '@/api/oa/erp/budgetInfo/rd/rdBudgetTechCost/types';
const TECH_TYPE = {
TECH_CONSULT: '1', //技术咨询开发
EXPERT_MEETING: '2', //专家咨询-会议形式
EXPERT_COMM: '3' //专家咨询-通讯形式
};
// Props
const props = defineProps<{
projectId?: string;
}>();
// Emits
const emit = defineEmits<{
update: [
data: {
techConsultTotal: number;
laborTotal: number;
serviceTotal: number;
}
];
}>();
// 响应式数据
const loading = ref(false);
// 技术咨询开发相关
const techConsultList = ref<rdBudgetTechCostVO[]>([]);
const selectedTechRows = ref<rdBudgetTechCostVO[]>([]);
const techAddRowCount = ref(1);
// 专家咨询-会议形式相关
const expertMeetingList = ref<rdBudgetTechCostVO[]>([]);
const selectedExpertMeetingRows = ref<rdBudgetTechCostVO[]>([]);
const expertMeetingAddRowCount = ref(1);
// 专家咨询-通讯形式相关
const expertCommList = ref<rdBudgetTechCostVO[]>([]);
const selectedExpertCommRows = ref<rdBudgetTechCostVO[]>([]);
const expertCommAddRowCount = ref(1);
const toDeletedTechCostIdList = ref([]);
// 新产品设计费合计方法
const getProductDesignSummaries = (param: any) => {
const { columns, data } = param;
const sums: any[] = [];
columns.forEach((column: any, index: number) => {
if (index === 2) {
sums[index] = '合计';
return;
}
if (column.property === 'price') {
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 techConsultSubtotal = computed(() => {
return techConsultList.value.reduce((sum, item) => sum + (Number(item.price) || 0), 0);
});
const expertMeetingSubtotal = computed(() => {
return expertMeetingList.value.reduce((sum, item) => {
const amountInTenThousand = Number(item.price) || 0;
const formattedAmount = parseFloat(amountInTenThousand.toFixed(2));
return sum + formattedAmount;
}, 0);
});
const expertCommSubtotal = computed(() => {
return expertCommList.value.reduce((sum, item) => {
const amountInTenThousand = Number(item.price) || 0;
const formattedAmount = parseFloat(amountInTenThousand.toFixed(2));
return sum + formattedAmount;
}, 0);
});
const expertConsultSubtotal = computed(() => {
return expertMeetingSubtotal.value + expertCommSubtotal.value;
});
// 格式化数字,元转换为万元
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 getExpertMeetingSummaries = (param: any) => {
const { columns, data } = param;
const sums: any[] = [];
columns.forEach((column: any, index: number) => {
if (index === 2) {
sums[index] = '小计';
return;
}
if (column.property === 'price') {
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 calculateExpertMeetingAmount = () => {
// expertMeetingList.value.forEach((item) => {
// const expertCount = item.peopleNumber || 0;
// const days = item.days || 0;
// // 公式1如果天数<2,则金额等于专家人数*天数*600如果天数>=2,则金额等于(专家人数*2*600+专家人数*(天数-2)*300)
// if (days < 2) {
// item.price = expertCount * days * 600;
// } else {
// item.price = expertCount * 2 * 600 + expertCount * (days - 2) * 300;
// }
// });
// };
// 专家咨询通讯形式合计方法
const getExpertCommSummaries = (param: any) => {
const { columns, data } = param;
const sums: any[] = [];
columns.forEach((column: any, index: number) => {
if (index === 2) {
sums[index] = '小计';
return;
}
if (column.property === 'price') {
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 calculateExpertCommAmount = () => {
// expertCommList.value.forEach((item) => {
// // 金额 = 人数 * 次数 * 70 / 10000
// item.price = (item.peopleNumber || 0) * (item.frequency || 0) * 70;
// });
// };
const getTechAmount = () => {
return {
expertConsultTotal: (Number(expertConsultSubtotal.value) || 0) * 10000,
techConsultTotal: (Number(techConsultSubtotal.value) || 0) * 10000
};
};
// 技术咨询开发相关方法
const confirmTechAdd = () => {
const currentSortOrder =
techConsultList.value && techConsultList.value.length > 0 ? Math.max(...techConsultList.value.map((item) => item.sortOrder)) : 0;
for (let i = 0; i < techAddRowCount.value; i++) {
const newItem: rdBudgetTechCostVO = {
sortOrder: currentSortOrder + i + 1,
techType: TECH_TYPE.TECH_CONSULT,
techContent: '',
unitId: undefined,
remark: '',
price: 0
};
techConsultList.value.push(newItem);
}
};
const handleTechSelectionChange = (selection: rdBudgetTechCostVO[]) => {
selectedTechRows.value = selection;
};
// 删除单行
const handleTechDelete = (index: number, row: rdBudgetTechCostVO) => {
techConsultList.value.splice(index, 1);
// 重新编号
techConsultList.value.forEach((item, index) => {
item.sortOrder = index + 1;
});
if (row.techCostId) {
toDeletedTechCostIdList.value.push(row.techCostId);
}
};
// 批量删除
const handleTechBatchDelete = () => {
if (selectedTechRows.value.length === 0) {
ElMessage.warning('请选择要删除的行');
return;
}
selectedTechRows.value.forEach((selectedRow) => {
if (selectedRow.techCostId) {
toDeletedTechCostIdList.value.push(selectedRow.techCostId);
}
});
techConsultList.value = techConsultList.value.filter((item) => !selectedTechRows.value.includes(item));
// 重新编号
techConsultList.value.forEach((item, index) => {
item.sortOrder = index + 1;
});
selectedTechRows.value = [];
};
// 专家咨询-会议形式相关方法
const confirmExpertMeetingAdd = () => {
const currentSortOrder =
expertMeetingList.value && expertMeetingList.value.length > 0 ? Math.max(...expertMeetingList.value.map((item) => item.sortOrder)) : 0;
for (let i = 0; i < expertMeetingAddRowCount.value; i++) {
const newItem: rdBudgetTechCostVO = {
sortOrder: currentSortOrder + i + 1,
techType: TECH_TYPE.EXPERT_MEETING,
techContent: '',
peopleNumber: undefined,
days: undefined,
price: 0
};
expertMeetingList.value.push(newItem);
}
};
const handleExpertMeetingSelectionChange = (selection: rdBudgetTechCostVO[]) => {
selectedExpertMeetingRows.value = selection;
};
// 删除单行
const handleExpertMeetingDelete = (index: number, row: rdBudgetTechCostVO) => {
expertMeetingList.value.splice(index, 1);
// 重新编号
expertMeetingList.value.forEach((item, index) => {
item.sortOrder = index + 1;
});
if (row.techCostId) {
toDeletedTechCostIdList.value.push(row.techCostId);
}
};
// 批量删除
const handleExpertMeetingBatchDelete = () => {
if (selectedExpertMeetingRows.value.length === 0) {
ElMessage.warning('请选择要删除的行');
return;
}
selectedExpertMeetingRows.value.forEach((selectedRow) => {
if (selectedRow.techCostId) {
toDeletedTechCostIdList.value.push(selectedRow.techCostId);
}
});
expertMeetingList.value = expertMeetingList.value.filter((item) => !selectedExpertMeetingRows.value.includes(item));
// 重新编号
expertMeetingList.value.forEach((item, index) => {
item.sortOrder = index + 1;
});
selectedExpertMeetingRows.value = [];
};
// 专家咨询-通讯形式相关方法
const confirmExpertCommAdd = () => {
const currentSortOrder =
expertCommList.value && expertCommList.value.length > 0 ? Math.max(...expertCommList.value.map((item) => item.sortOrder)) : 0;
for (let i = 0; i < expertCommAddRowCount.value; i++) {
const newItem: rdBudgetTechCostVO = {
sortOrder: currentSortOrder + i + 1,
techType: TECH_TYPE.EXPERT_COMM,
techContent: '',
peopleNumber: undefined,
frequency: undefined,
price: 0
};
expertCommList.value.push(newItem);
}
};
const handleExpertCommSelectionChange = (selection: rdBudgetTechCostVO[]) => {
selectedExpertCommRows.value = selection;
};
// 删除单行
const handleExpertCommDelete = (index: number, row: rdBudgetTechCostVO) => {
expertCommList.value.splice(index, 1);
// 重新编号
expertCommList.value.forEach((item, index) => {
item.sortOrder = index + 1;
});
if (row.techCostId) {
toDeletedTechCostIdList.value.push(row.techCostId);
}
};
// 批量删除
const handleExpertCommBatchDelete = () => {
if (selectedExpertCommRows.value.length === 0) {
ElMessage.warning('请选择要删除的行');
return;
}
selectedExpertCommRows.value.forEach((selectedRow) => {
if (selectedRow.techCostId) {
toDeletedTechCostIdList.value.push(selectedRow.techCostId);
}
});
expertCommList.value = expertCommList.value.filter((item) => !selectedExpertCommRows.value.includes(item));
// 重新编号
expertCommList.value.forEach((item, index) => {
item.sortOrder = index + 1;
});
selectedExpertCommRows.value = [];
};
// 监听projectId变化重新加载数据
watch(
() => props.projectId,
(newProjectId) => {
if (newProjectId) {
}
},
{ immediate: true }
);
// 暴露方法供父组件调用
defineExpose({
techConsultList,
expertMeetingList,
expertCommList,
toDeletedTechCostIdList,
getTechAmount
});
</script>
<style scoped>
.labor-service-container {
padding: 0;
}
:deep(.el-table) {
margin-bottom: 16px;
}
:deep(.el-input-number) {
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.bg-gray-50 {
background-color: #f5f7fa;
}
.p-2 {
padding: 8px;
}
.p-3 {
padding: 12px;
}
.rounded {
border-radius: 4px;
}
.text-right {
text-align: right;
}
.font-medium {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
</style>