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.

641 lines
22 KiB
Vue

<template>
<div class="project-budget-container">
<!-- 审批按钮组件 -->
<approvalButton
@submitForm="handleSave"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="searchForm.budgetId"
:status="searchForm.flowStatus"
:pageType="routeParams.type"
:mode="false"
/>
<!-- 顶部表单区域 -->
<el-card class="mb-6">
<el-form :model="searchForm" label-width="80px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="项目名称">
<el-input v-model="searchForm.projectName" placeholder="请选择项目名称" readonly @click="showProjectSelectDialog">
<template #append>
<el-icon @click="showProjectSelectDialog">
<el-icon :size="16">
<Search />
</el-icon>
</el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="项目号">
<el-input v-model="searchForm.projectCode" placeholder="项目号" readonly />
</el-form-item>
</el-col>
<!-- <el-col :span="12" class="text-right">-->
<!-- <el-button type="primary" @click="exportBudget" size="default">-->
<!-- <el-icon><Download /></el-icon>-->
<!-- 导出-->
<!-- </el-button>-->
<!-- </el-col>-->
</el-row>
</el-form>
</el-card>
<!-- 中间标签页区域 -->
<!-- 研发项目预算 -->
<el-card class="mb-6" v-if="searchForm.projectCategory === PROJECT_CATEGORY.RD">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="预算表" name="budgetTable">
<RdBudgetTable ref="rdBudgetTableRef" :projectId="searchForm.projectId" :costData="costData" :projectInfo="projectInfo" />
</el-tab-pane>
<el-tab-pane label="设备费" name="equipmentCost">
<RdEquipmentCost ref="rdEquipmentCostRef" :projectId="searchForm.projectId" @update="handleEquipmentCostUpdate" />
</el-tab-pane>
<el-tab-pane label="材料费" name="materialCost">
<RdMaterialCost ref="rdMaterialCostRef" :projectId="searchForm.projectId" @update="handleMaterialCostUpdate" />
</el-tab-pane>
<el-tab-pane label="会议差旅交流" name="travelMeetingExchange">
<RdTravelMeetingExchange ref="rdTravelMeetingExchangeRef" :projectId="searchForm.projectId" @update="handleTravelMeetingExchangeUpdate" />
</el-tab-pane>
<el-tab-pane label="人工劳务咨询" name="laborService">
<RdLaborService ref="rdLaborServiceRef" :projectId="searchForm.projectId" @update="handleLaborServiceUpdate" />
</el-tab-pane>
<el-tab-pane label="资料文献费" name="literatureCost">
<RdLiteratureCost ref="rdLiteratureCostRef" :projectId="searchForm.projectId" @update="handleLiteratureCostUpdate" />
</el-tab-pane>
<el-tab-pane label="测试化验费" name="testingCost">
<RdTestingCost ref="rdTestingCostRef" :projectId="searchForm.projectId" @update="handleTestingCostUpdate" />
</el-tab-pane>
<el-tab-pane label="其他" name="otherCost">
<RdOtherCost ref="rdOtherCostRef" :projectId="searchForm.projectId" @update="handleOtherCostUpdate" />
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 市场项目预算 -->
<el-card class="tab-card" shadow="never" v-if="searchForm.projectCategory === PROJECT_CATEGORY.MARKET">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="预算表" name="budget">
<BudgetTable ref="budgetTableRef" :projectInfo="projectInfo" />
</el-tab-pane>
<el-tab-pane label="材料费" name="material">
<MaterialCost ref="materialCostRef" />
</el-tab-pane>
<el-tab-pane label="人工费" name="labor">
<LaborCost ref="laborCostRef" />
</el-tab-pane>
<el-tab-pane label="安装费" name="installation">
<InstallationCost ref="installationCostRef" />
</el-tab-pane>
<el-tab-pane label="差旅费" name="travel">
<TravelCost ref="travelCostRef" />
</el-tab-pane>
<el-tab-pane label="其他" name="other">
<OtherCost ref="otherCostRef" />
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 项目选择弹窗 -->
<el-dialog v-model="projectSelectDialogVisible" title="选择项目" width="800px">
<div class="mb-4">
<el-input v-model="projectSearchForm.keyword" placeholder="请输入项目名称或项目号" clearable @input="handleProjectSearch">
<template #append>
<el-button @click="handleProjectSearch"></el-button>
</template>
</el-input>
</div>
<el-table :data="projectList" border @row-click="selectProject" style="width: 100%" v-loading="projectLoading" highlight-current-row>
<el-table-column prop="projectCode" label="项目号" width="150" />
<el-table-column prop="projectName" label="项目名称" min-width="180" />
<el-table-column prop="businessDirection" label="业务方向" width="120">
<template #default="scope">
<dict-tag :options="business_direction" :value="scope.row.businessDirection" />
</template>
</el-table-column>
<el-table-column prop="projectCategory" label="项目类别" width="160">
<template #default="scope">
<dict-tag :options="project_category" :value="scope.row.projectCategory" />
</template>
</el-table-column>
<el-table-column prop="projectStatus" label="项目状态" width="100">
<template #default="scope">
<dict-tag :options="project_status" :value="scope.row.projectStatus" />
</template>
</el-table-column>
</el-table>
<div class="mt-4 flex justify-between items-center">
<pagination
v-show="projectTotal > 0"
:total="projectTotal"
v-model:page="projectSearchForm.pageNum"
v-model:limit="projectSearchForm.pageSize"
@pagination="getProjectList"
/>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="projectSelectDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmProjectSelection"></el-button>
</span>
</template>
</el-dialog>
<!-- 提交审批组件 -->
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<!-- 审批记录 -->
<approvalRecord ref="approvalRecordRef" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { listProjectInfo, addErpBudgetInfo, updateErpBudgetInfo, getErpBudgetInfo } from '@/api/oa/erp/budgetInfo';
import { ProjectInfoVO, ProjectInfoQuery } from '@/api/oa/erp/projectInfo/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { business_direction, project_status, project_category } = toRefs<any>(
proxy?.useDict('business_direction', 'project_status', 'project_category')
);
//研发项目tab
import RdBudgetTable from './rd/BudgetTable.vue';
import RdEquipmentCost from './rd/EquipmentCost.vue';
import RdMaterialCost from './rd/MaterialCost.vue';
import RdTravelMeetingExchange from './rd/TravelMeetingExchange.vue';
import RdLaborService from './rd/LaborService.vue';
import RdLiteratureCost from './rd/LiteratureCost.vue';
import RdTestingCost from './rd/TestingCost.vue';
import RdOtherCost from './rd/OtherCost.vue';
//市场项目tab
import BudgetTable from './market/BudgetTable.vue';
import MaterialCost from './market/MaterialCost.vue';
import LaborCost from './market/LaborCost.vue';
import InstallationCost from './market/InstallationCost.vue';
import TravelCost from './market/TravelCost.vue';
import OtherCost from './market/OtherCost.vue';
// 定义成本数据类型
interface CostData {
equipmentCost: number;
materialCost: number;
travelCost: number;
meetingCost: number;
internationalExchangeCost: number;
consultingDevelopmentCost: number;
laborCost: number;
serviceCost: number;
literatureCost: number;
testingCost: number;
otherCost: number;
}
// 研发项目预算tab组件引用
const rdBudgetTableRef = ref();
const rdEquipmentCostRef = ref();
const rdMaterialCostRef = ref();
const rdTravelMeetingExchangeRef = ref();
const rdLaborServiceRef = ref();
const rdLiteratureCostRef = ref();
const rdTestingCostRef = ref();
const rdOtherCostRef = ref();
// 市场项目预算tab组件引用
const budgetTableRef = ref();
const materialCostRef = ref();
const laborCostRef = ref();
const installationCostRef = ref();
const travelCostRef = ref();
const otherCostRef = ref();
const PROJECT_CATEGORY = reactive({
MARKET: '1', //销售市场项目
RD: '2' //研发项目
});
const BUSINESS_STATUS = reactive({
DRAFT: '1', //暂存
WAITING: '2', //审批中
AVAILABLE: '3' //可用
});
// 响应式数据
// 将activeTab从computed改为ref使其可变
const activeTab = ref('budget');
const buttonLoading = ref(false);
const searchForm = reactive({
budgetId: undefined,
projectId: '',
projectName: '',
projectCode: '',
projectCategory: '1',
flowStatus: ''
});
// 监听项目类别变化,更新默认标签页
watch(
() => searchForm.projectCategory,
(newCategory) => {
if (newCategory === PROJECT_CATEGORY.RD) {
activeTab.value = 'budgetTable';
} else if (newCategory === PROJECT_CATEGORY.MARKET) {
activeTab.value = 'budget';
}
}
);
// 项目信息
const projectInfo = reactive<ProjectInfoVO>({
projectName: '',
projectCode: '',
projectCategory: ''
});
// 市场预算成本数据
const marketCostData = reactive<CostData>({
materialCost: 0,
laborCost: 0,
installCost: 0,
travelCost: 0,
serviceCost: 0,
literatureCost: 0,
testingCost: 0,
otherCost: 0
});
// 研发预算成本数据
const costData = reactive<CostData>({
equipmentCost: 0,
materialCost: 0,
travelCost: 0,
meetingCost: 0,
internationalExchangeCost: 0,
consultingDevelopmentCost: 0,
laborCost: 0,
serviceCost: 0,
literatureCost: 0,
testingCost: 0,
otherCost: 0
});
// 项目选择弹窗相关
const projectSelectDialogVisible = ref(false);
const selectedProject = ref<ProjectInfoVO | null>(null);
const projectList = ref<ProjectInfoVO[]>([]);
const projectTotal = ref(0);
const projectLoading = ref(true);
const projectSearchForm = reactive({
keyword: '',
pageNum: 1,
pageSize: 10
});
/** 查询项目信息列表 */
const getProjectList = async () => {
projectLoading.value = true;
const res = await listProjectInfo(projectSearchForm);
console.log(res);
projectList.value = res.rows;
projectTotal.value = res.total;
projectLoading.value = false;
};
// 标签页切换处理
const handleTabClick = (tab: any) => {
activeTab.value = tab.paneName;
};
// 显示项目选择弹窗
const showProjectSelectDialog = () => {
projectSelectDialogVisible.value = true;
selectedProject.value = undefined;
getProjectList();
};
// 项目搜索
const handleProjectSearch = () => {
projectSearchForm.pageNum = 1;
};
// 项目分页大小变化
const handleProjectPageSizeChange = (size: number) => {
projectSearchForm.pageSize = size;
};
// 项目当前页变化
const handleProjectCurrentPageChange = (current: number) => {
projectSearchForm.pageNum = current;
};
// 选择项目
const selectProject = (row: ProjectInfoVO) => {
selectedProject.value = row;
};
// 确认项目选择
const confirmProjectSelection = () => {
if (!selectedProject.value) {
ElMessage.warning('请选择一个项目');
return;
}
// 更新项目信息
searchForm.projectId = selectedProject.value.projectId;
searchForm.projectName = selectedProject.value.projectName;
searchForm.projectCode = selectedProject.value.projectCode;
searchForm.projectCategory = selectedProject.value.projectCategory;
projectInfo.projectId = selectedProject.value.projectId;
projectInfo.projectName = selectedProject.value.projectName;
projectInfo.projectCode = selectedProject.value.projectCode;
projectInfo.projectCategory = selectedProject.value.projectCategory;
projectSelectDialogVisible.value = false;
// ElMessage.success('项目选择成功');
// 重置所有成本数据
Object.keys(costData).forEach((key) => {
costData[key as keyof CostData] = 0;
});
};
// 设备费更新处理
const handleEquipmentCostUpdate = (data: { equipmentCost: number }) => {
costData.equipmentCost = data.equipmentCost;
if (rdBudgetTableRef.value) {
rdBudgetTableRef.value.updateCostData(costData);
}
};
// 材料费更新处理
const handleMaterialCostUpdate = (data: { mainMaterialCost: number; otherMaterialCost: number; totalAmount: number }) => {
costData.materialCost = data.materialCost;
if (rdBudgetTableRef.value) {
rdBudgetTableRef.value.updateCostData(costData);
}
};
// 会议差旅交流更新处理
const handleTravelMeetingExchangeUpdate = (data: { travelCost: number; meetingCost: number; internationalExchangeCost: number }) => {
costData.travelCost = data.travelCost;
costData.meetingCost = data.meetingCost;
costData.internationalExchangeCost = data.internationalExchangeCost;
if (rdBudgetTableRef.value) {
rdBudgetTableRef.value.updateCostData(costData);
}
};
// 人工劳务咨询更新处理
const handleLaborServiceUpdate = (data: { techConsultTotal: number; laborTotal: number; serviceTotal: number }) => {
costData.consultingDevelopmentCost = data.techConsultTotal;
costData.laborCost = data.laborTotal;
costData.serviceCost = data.serviceTotal;
if (rdBudgetTableRef.value) {
rdBudgetTableRef.value.updateCostData(costData);
}
};
// 资料文献费更新处理
const handleLiteratureCostUpdate = (data: { literatureCost: number }) => {
costData.literatureCost = data.literatureCost;
if (rdBudgetTableRef.value) {
rdBudgetTableRef.value.updateCostData(costData);
}
};
// 测试化验费更新处理
const handleTestingCostUpdate = (data: { totalAmount: number }) => {
costData.testingCost = data.totalAmount;
if (rdBudgetTableRef.value) {
rdBudgetTableRef.value.updateCostData(costData);
}
};
// 其他费用更新处理
const handleOtherCostUpdate = (data: { totalAmount: number }) => {
costData.otherCost = data.totalAmount;
if (rdBudgetTableRef.value) {
rdBudgetTableRef.value.updateCostData(costData);
}
};
// 更新市场项目预算预算表数据
const updateBudgetTable = () => {
if (!budgetTableRef.value) return;
// 获取各tab页的合计数据
const materialTotal = materialCostRef.value?.getTotalAmount() || { budgetAmount: 0, reducedAmount: 0 };
const laborTotal = laborCostRef.value?.getTotalAmount() || { budgetAmount: 0, reducedAmount: 0 };
const installationTotal = installationCostRef.value?.getTotalAmount() || { budgetAmount: 0, reducedAmount: 0 };
const travelTotal = travelCostRef.value?.getTotalAmount() || { budgetAmount: 0, reducedAmount: 0 };
const otherTotal = otherCostRef.value?.getTotalAmount() || { budgetAmount: 0, reducedAmount: 0 };
// 更新预算表
budgetTableRef.value.updateBudgetDetailData('材料费', materialTotal.budgetAmount, materialTotal.reducedAmount);
budgetTableRef.value.updateBudgetDetailData('人工费', laborTotal.budgetAmount, laborTotal.reducedAmount);
budgetTableRef.value.updateBudgetDetailData('安装费', installationTotal.budgetAmount, installationTotal.reducedAmount);
budgetTableRef.value.updateBudgetDetailData('差旅费', travelTotal.budgetAmount, travelTotal.reducedAmount);
budgetTableRef.value.updateBudgetDetailData('其他费用', otherTotal.budgetAmount, otherTotal.reducedAmount);
};
// 新增一个方法用于在tab切换时更新预算数据
const updateBudgetDataOnTabChange = () => {
nextTick(() => {
if (searchForm.projectCategory === PROJECT_CATEGORY.RD) {
// 确保调用更新方法时传递正确的参数
if (rdEquipmentCostRef.value?.getTotalAmount) {
handleEquipmentCostUpdate({ equipmentCost: rdEquipmentCostRef.value.getTotalAmount() });
}
if (rdMaterialCostRef.value?.getTotalAmount) {
const materialTotal = rdMaterialCostRef.value.getTotalAmount();
handleMaterialCostUpdate({
mainMaterialCost: materialTotal.mainMaterialCost || 0,
otherMaterialCost: materialTotal.otherMaterialCost || 0,
materialCost: (materialTotal.mainMaterialCost || 0) + (materialTotal.otherMaterialCost || 0)
});
}
// 其他方法类似处理...
} else if (searchForm.projectCategory === PROJECT_CATEGORY.MARKET) {
updateBudgetTable();
}
});
};
// 监听tab切换更新预算表数据
watch(activeTab, () => {
updateBudgetDataOnTabChange();
});
// 导出预算
const exportBudget = () => {
if (!searchForm.projectId) {
ElMessage.warning('请先选择项目');
return;
}
ElMessage.success('导出预算成功');
};
// 保存预算
const handleSave = async (status: string, mode: boolean) => {
if (!searchForm.projectId) {
ElMessage.warning('请先选择项目');
return;
}
if (searchForm.projectCategory === PROJECT_CATEGORY.RD) {
handleEquipmentCostUpdate();
handleMaterialCostUpdate();
handleTravelMeetingExchangeUpdate();
handleLaborServiceUpdate();
handleLiteratureCostUpdate();
handleTestingCostUpdate();
handleOtherCostUpdate();
} else if (searchForm.projectCategory === PROJECT_CATEGORY.MARKET) {
updateBudgetTable();
}
try {
// 收集所有标签页的数据
if (searchForm.projectCategory === PROJECT_CATEGORY.RD) {
const allData = {
projectId: searchForm.projectId,
costData: { ...costData },
equipmentCostData: rdEquipmentCostRef.value?.equipmentList || [],
materialCostData: rdMaterialCostRef.value?.materialList || [],
travelMeetingExchangeData: {
travelList: rdTravelMeetingExchangeRef.value?.travelList || [],
meetingList: rdTravelMeetingExchangeRef.value?.meetingList || [],
internationalExchangeList: rdTravelMeetingExchangeRef.value?.internationalExchangeList || []
},
laborServiceData: {
techConsultList: rdLaborServiceRef.value?.techConsultList || [],
expertMeetingList: rdLaborServiceRef.value?.expertMeetingList || [],
expertCommList: rdLaborServiceRef.value?.expertCommList || [],
laborList: rdLaborServiceRef.value?.laborList || [],
serviceList: rdLaborServiceRef.value?.serviceList || []
},
literatureCostData: {
materialsList: rdLiteratureCostRef.value?.materialsList || [],
literatureRetrieval: rdLiteratureCostRef.value?.literatureRetrieval || {},
softwareList: rdLiteratureCostRef.value?.softwareList || []
},
testingCostData: rdTestingCostRef.value?.testingList || [],
otherCostData: rdOtherCostRef.value?.otherCostList || []
};
} else if (searchForm.projectCategory === PROJECT_CATEGORY.MARKET) {
const budgetDetailData = budgetTableRef.value?.budgetDetailData;
let budgetForm = budgetTableRef.value?.budgetForm;
budgetForm.budgetStatus = status === 'draft' ? BUSINESS_STATUS.DRAFT : BUSINESS_STATUS.WAITING;
budgetForm.flowStatus = status === 'draft' ? 'draft' : 'waiting';
budgetForm.erpBudgetDetailList = budgetDetailData;
budgetForm.erpBudgetMaterialCostList = materialCostRef.value?.budgetMaterialCostList;
console.log(budgetForm);
buttonLoading.value = true;
if (budgetForm.budgetId) {
await updateErpBudgetInfo(budgetForm).finally(() => (buttonLoading.value = false));
} else {
await addErpBudgetInfo(budgetForm).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
}
} catch (error) {
ElMessage.error('保存失败');
console.error('保存失败:', error);
}
};
// 关闭页面
const handleClose = () => {
// 可以添加确认对话框
// 这里简单实现返回上一页
window.history.back();
};
const route = useRoute();
// 初始化
onMounted(() => {
// 可以在这里加载初始数据
routeParams.value.type = 'add';
nextTick(async () => {
// 获取路由参数
routeParams.value = route.query;
const id = routeParams.value.id as string | number;
if (id && (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval')) {
proxy?.$modal.loading('正在加载数据,请稍后...');
const res = await getErpBudgetInfo(id);
Object.assign(searchForm, res.data);
Object.assign(budgetTableRef.value?.budgetForm,res.data)
proxy?.$modal.closeLoading();
} else {
// 新增模式:如果已有合同编号,禁用生成按钮
// if (form.value.contractCode) {
// isCodeGenerated.value = true;
// } else if (form.value.contractFlag === '1') {
// // 如果有合同但没有编号,允许生成
// isCodeGenerated.value = false;
// }
}
});
});
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import { getContractInfo } from '@/api/oa/erp/contractInfo';
// 路由参数
const routeParams = ref<Record<string, any>>({});
// 审批相关组件引用
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
// 审批
const approvalVerifyOpen = async () => {
await submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
// 审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(searchForm.budgetId);
};
</script>
<style scoped>
.project-budget-container {
padding: 20px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.justify-end {
justify-content: flex-end;
}
.gap-2 {
gap: 8px;
}
.gap-4 {
gap: 16px;
}
.mb-6 {
margin-top: 12px;
margin-bottom: 6px;
}
</style>