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.

603 lines
22 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="p-2">
<el-card shadow="never">
<div v-if="showApprovalButton" class="mb-3 flex items-center justify-between">
<div>
<el-button
v-if="showSubmitButtons"
type="info"
:loading="draftLoading"
:disabled="submitLoading"
@click="handleSubmitAction('draft')"
>暂存</el-button>
<el-button
v-if="showSubmitButtons"
type="primary"
:loading="submitLoading"
:disabled="draftLoading"
@click="handleSubmitAction('submit')"
>提交</el-button>
<el-button
v-if="showApprovalAction"
type="primary"
:disabled="draftLoading || submitLoading"
@click="approvalVerifyOpen"
>审批</el-button>
<el-button
v-if="showProgressButton"
type="primary"
:disabled="draftLoading || submitLoading"
@click="handleApprovalRecord"
>流程进度</el-button>
</div>
<div>
<el-button @click="goBack">返回</el-button>
</div>
</div>
<div v-else-if="showLimitedActions" class="mb-3 text-right">
<el-button type="primary" :loading="buttonLoading" @click="handleSave">保存</el-button>
<el-button @click="goBack">返回</el-button>
</div>
<el-form ref="projectPlanFormRef" :model="form" :rules="rules" :disabled="isViewMode" label-width="120px">
<el-divider content-position="left">基本信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目名称" prop="projectId">
<el-select v-model="form.projectId" placeholder="请选择项目" filterable :disabled="!isBasicEditable" @change="handleProjectChange">
<el-option v-for="item in projectInfoList" :key="item.projectId" :label="item.projectName" :value="item.projectId"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目经理" prop="managerId">
<el-select v-model="form.managerId" placeholder="请选择项目经理" filterable disabled>
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="部门负责人" prop="chargeId">
<el-select v-model="form.chargeId" placeholder="请选择部门负责人" filterable disabled>
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="关联合同" prop="contractId">
<el-select v-model="form.contractId" placeholder="请选择合同" filterable clearable :disabled="!isBasicEditable">
<el-option v-for="item in contractInfoList" :key="item.contractId" :label="item.contractName" :value="item.contractId"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="付款方式" prop="paymentMethod">
<el-input v-model="form.paymentMethod" placeholder="3-3-3-1" :disabled="!isBasicEditable" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目计划状态" prop="projectPlanStatus">
<el-select v-model="form.projectPlanStatus" placeholder="请选择项目计划状态" :disabled="!isBasicEditable">
<el-option v-for="dict in project_plan_status" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" :disabled="!isBasicEditable" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">项目阶段计划</el-divider>
<el-row :gutter="10" class="mb8" v-if="isBasicEditable">
<el-col :span="1.5">
<el-button type="primary" icon="Plus" @click="handleAddStage">添加阶段</el-button>
</el-col>
</el-row>
<el-table :data="form.planStageList" border stripe max-height="600">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="项目阶段" width="150" align="center">
<template #default="scope">
<el-select v-model="scope.row.projectPhases" placeholder="请选择项目阶段" style="width: 100%" :disabled="!isBasicEditable">
<el-option v-for="dict in project_phases" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</template>
</el-table-column>
<el-table-column label="计划开始时间" width="160" align="center">
<template #default="scope">
<el-date-picker v-model="scope.row.planStartTime" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" :disabled="!isBasicEditable"/>
</template>
</el-table-column>
<el-table-column label="计划结束时间" width="160" align="center">
<template #default="scope">
<el-date-picker v-model="scope.row.planEndTime" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" :disabled="!isBasicEditable"/>
</template>
</el-table-column>
<el-table-column label="回款阶段" width="150" align="center">
<template #default="scope">
<el-select v-model="scope.row.collectionStage" placeholder="请选择回款阶段" style="width: 100%" :disabled="!isBasicEditable">
<el-option v-for="dict in collection_stage" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</template>
</el-table-column>
<el-table-column label="回款比例(%)" width="120" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.repaymentRate" :min="0" :max="100" :precision="2" controls-position="right" style="width: 100%" :disabled="!isBasicEditable" @change="calculateRepaymentAmount(scope.row)"/>
</template>
</el-table-column>
<el-table-column label="预计回款金额" width="140" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.repaymentAmount" :min="0" :precision="2" controls-position="right" style="width: 100%" :disabled="!isBasicEditable"/>
</template>
</el-table-column>
<el-table-column label="预计回款时间" width="160" align="center">
<template #default="scope">
<el-date-picker v-model="scope.row.repaymentTime" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" :disabled="!isBasicEditable" @change="recalcReceivableDate(scope.row)"/>
</template>
</el-table-column>
<el-table-column label="回款延期天数" width="140" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.delayDay" :min="0" :step="1" controls-position="right" style="width: 100%" :disabled="!canEditLimited" @change="recalcReceivableDate(scope.row)"/>
</template>
</el-table-column>
<el-table-column label="应收款日期" width="160" align="center">
<template #default="scope">
<el-date-picker v-model="scope.row.receivableDate" type="date" value-format="YYYY-MM-DD" placeholder="自动计算" style="width: 100%" disabled/>
</template>
</el-table-column>
<el-table-column label="进度备注" width="200" align="center">
<template #default="scope">
<el-input v-model="scope.row.scheduleRemark" placeholder="请输入进度备注" :disabled="!canEditLimited"/>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center" fixed="right" v-if="isBasicEditable">
<template #default="scope">
<el-button type="danger" link icon="Delete" @click="handleDeleteStage(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-form>
</el-card>
<!-- 提交审批组件 -->
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<!-- 审批记录 -->
<approvalRecord ref="approvalRecordRef" />
</div>
</template>
<script setup lang="ts" name="ErpProjectPlanEdit">
import { ref, reactive, onMounted, getCurrentInstance, watch, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { getErpProjectPlan, addErpProjectPlan, updateErpProjectPlan, projectPlanSubmitAndFlowStart } from '@/api/oa/erp/erpProjectPlan';
import { ErpProjectPlanForm } from '@/api/oa/erp/erpProjectPlan/types';
import { listUser } from '@/api/system/user';
import { getErpProjectInfoList } from '@/api/oa/erp/projectInfo';
import { getErpContractInfoList } from '@/api/oa/erp/contractInfo';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import { useUserStore } from '@/store/modules/user';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { project_plan_status , project_phases, collection_stage } = toRefs<any>(
proxy?.useDict('project_plan_status', 'project_phases', 'collection_stage')
);
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
// 审批相关组件引用
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
// 路由参数
const routeParams = ref<any>({});
const syncRouteParams = () => {
const projectPlanId = (route.params.projectPlanId as string) || (route.query.id as string);
const type = (route.query.type as string) || (route.params.type as string) || 'edit';
const taskId = (route.query.taskId as string) || (route.params.taskId as string);
routeParams.value = {
projectPlanId,
type,
taskId
};
};
syncRouteParams();
watch(
() => route.fullPath,
() => {
syncRouteParams();
}
);
// approvalButton组件的pageType将'edit'转换为'update'
const approvalPageType = computed(() => {
if (routeParams.value.type === 'edit') {
return 'update';
}
return routeParams.value.type;
});
// 是否只读模式
const isViewMode = computed(() => ['view', 'approval'].includes(routeParams.value.type));
// 流程相关数据
const taskVariables = ref<any>({});
// 计算属性:判断基础信息是否可编辑
const isBasicEditable = computed(() => {
if (isViewMode.value) {
return false;
}
// 草稿、驳回、撤回均允许项目经理完整编辑
return (
form.value.projectPlanStatus === '1' ||
['back', 'cancel'].includes(form.value.flowStatus as string)
);
});
// 计算属性:判断是否可编辑延迟/备注等有限字段
const canEditLimited = computed(() => {
if (isViewMode.value) {
return false;
}
if (isBasicEditable.value) {
return true;
}
// 审批完成允许维护延期与进度备注
return form.value.projectPlanStatus === '3' || form.value.flowStatus === 'finish';
});
// 限制模式(审批完成,仅维护部分字段)
const isLimitedMode = computed(() => !isBasicEditable.value && canEditLimited.value);
// 按钮展示控制
const showApprovalButton = computed(() => {
if (isLimitedMode.value) {
return false;
}
if (routeParams.value.type === 'approval') {
return true;
}
return !isViewMode.value;
});
const showLimitedActions = computed(() => !isViewMode.value && isLimitedMode.value);
const showSubmitButtons = computed(() => {
if (approvalPageType.value === 'add') return true;
if (
approvalPageType.value === 'update' &&
form.value.flowStatus &&
['draft', 'cancel', 'back'].includes(form.value.flowStatus as string)
) {
return true;
}
return false;
});
const showApprovalAction = computed(() => {
return approvalPageType.value === 'approval' && form.value.flowStatus === 'waiting';
});
const showProgressButton = computed(() => {
return !!form.value.projectPlanId && form.value.flowStatus !== 'draft';
});
const projectPlanFormRef = ref<ElFormInstance>();
const buttonLoading = ref(false);
const draftLoading = ref(false);
const submitLoading = ref(false);
const currentAction = ref<'draft' | 'submit' | null>(null);
const projectInfoList = ref<any[]>([]);
const userList = ref<any[]>([]);
const contractInfoList = ref<any[]>([]);
const contractTotalPrice = ref<number>(0);
const form = ref<ErpProjectPlanForm>({
projectPlanId: undefined,
projectId: undefined,
managerId: undefined,
chargeId: undefined,
paymentMethod: undefined,
projectPlanStatus: '1',
flowStatus: 'draft',
contractId: undefined,
remark: undefined,
planStageList: []
});
const rules = reactive({
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
managerId: [{ required: true, message: '请选择项目经理', trigger: 'change' }]
});
/** 获取项目列表 */
const getProjectInfoList = async () => {
const res = await getErpProjectInfoList({});
projectInfoList.value = res.data;
};
/** 获取用户列表 */
const getUserList = async () => {
const res = await listUser({ pageNum: 1, pageSize: 9999 });
userList.value = res.rows;
};
/** 获取合同列表 */
const getContractInfoList = async () => {
const res = await getErpContractInfoList({});
contractInfoList.value = res.data;
};
/** 添加阶段 */
const handleAddStage = () => {
const newStage = {
planStageId: undefined,
projectId: form.value.projectId,
projectPlanId: form.value.projectPlanId,
projectPhases: undefined,
planStartTime: undefined,
planEndTime: undefined,
collectionStage: undefined,
repaymentRate: undefined,
repaymentAmount: undefined,
repaymentTime: undefined,
delayDay: undefined,
receivableDate: undefined,
reasonsExplanation: undefined,
scheduleRemark: undefined,
realStartTime: undefined,
realEndTime: undefined,
sortOrder: (form.value.planStageList?.length || 0) + 1
};
if (!form.value.planStageList) {
form.value.planStageList = [];
}
form.value.planStageList.push(newStage);
};
/** 删除阶段 */
const handleDeleteStage = (index: number) => {
form.value.planStageList?.splice(index, 1);
};
/** 处理项目选择变化,自动填充项目经理和部门负责人 */
const handleProjectChange = (projectId: number) => {
if (!projectId) {
form.value.managerId = undefined;
form.value.chargeId = undefined;
return;
}
const project = projectInfoList.value.find(p => p.projectId === projectId);
if (project) {
form.value.managerId = project.managerId;
form.value.chargeId = project.chargeId;
}
};
/** 根据回款比例计算回款金额 */
const calculateRepaymentAmount = (row: any) => {
if (row.repaymentRate && contractTotalPrice.value > 0) {
row.repaymentAmount = parseFloat((contractTotalPrice.value * row.repaymentRate / 100).toFixed(2));
}
};
/** 根据回款时间与延期天数生成应收款日期 */
const recalcReceivableDate = (row: any) => {
if (!row) return;
const time = row.repaymentTime;
const delay = Number(row.delayDay || 0);
if (!time) {
row.receivableDate = undefined;
return;
}
const base = new Date(time as string);
if (isNaN(base.getTime())) {
row.receivableDate = undefined;
return;
}
const target = new Date(base.getTime());
target.setDate(target.getDate() + delay);
// 使用系统已有的parseTime进行格式化保持与全局一致
row.receivableDate = proxy?.parseTime ? proxy.parseTime(target, '{y}-{m}-{d}') : target.toISOString().slice(0, 10);
};
/** 监听合同变化,更新合同总价 */
watch(() => form.value.contractId, (newContractId) => {
if (newContractId) {
const contract = contractInfoList.value.find(c => c.contractId === newContractId);
if (contract) {
contractTotalPrice.value = contract.totalPrice || 0;
}
} else {
contractTotalPrice.value = 0;
}
});
/** 提交表单 */
const submitForm = async (status = 'draft') => {
currentAction.value = status as 'draft' | 'submit';
if (currentAction.value === 'draft') {
draftLoading.value = true;
} else if (currentAction.value === 'submit') {
submitLoading.value = true;
}
// 权限校验:只有项目经理才能提交(超级管理员跳过校验)
if (!form.value.managerId) {
proxy?.$modal.msgError('请先选择项目');
resetActionLoading();
return;
}
// 超级管理员跳过权限校验
const isSuperAdmin = userStore.roles.includes('admin') || userStore.roles.includes('superadmin');
if (!isSuperAdmin && userStore.userId !== form.value.managerId) {
proxy?.$modal.msgError('只有项目经理才能提交项目计划');
resetActionLoading();
return;
}
projectPlanFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
// 更新阶段的projectId
if (form.value.planStageList) {
form.value.planStageList.forEach(stage => {
stage.projectId = form.value.projectId;
});
}
// 提交审批
if (status === 'submit') {
const project = projectInfoList.value.find(p => p.projectId === form.value.projectId);
form.value.flowCode = 'OAPS';
form.value.variables = {
projectId: form.value.projectId,
projectName: project?.projectName,
managerId: form.value.managerId
};
form.value.bizExt = {
businessTitle: '项目计划审批',
businessCode: project?.projectName
};
form.value.projectPlanStatus = '2';
form.value.flowStatus = 'waiting';
const res = await projectPlanSubmitAndFlowStart(form.value).finally(() => (buttonLoading.value = false));
form.value = res.data;
buttonLoading.value = false;
proxy?.$modal.msgSuccess('操作成功');
proxy?.$tab.closePage();
router.go(-1);
} else {
// 暂存
if (status === 'draft') {
form.value.projectPlanStatus = '1';
form.value.flowStatus = 'draft';
}
if (form.value.projectPlanId) {
await updateErpProjectPlan(form.value).finally(() => (buttonLoading.value = false));
} else {
await addErpProjectPlan(form.value).finally(() => (buttonLoading.value = false));
}
buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功');
proxy?.$tab.closePage();
router.go(-1);
}
} finally {
buttonLoading.value = false;
resetActionLoading();
}
} else {
resetActionLoading();
buttonLoading.value = false;
}
});
};
const handleSubmitAction = (type: 'draft' | 'submit') => {
if (draftLoading.value || submitLoading.value) return;
submitForm(type);
};
const resetActionLoading = () => {
if (currentAction.value === 'draft') {
draftLoading.value = false;
}
if (currentAction.value === 'submit') {
submitLoading.value = false;
}
currentAction.value = null;
};
/** 保存按钮 - 保存为草稿 */
const handleSave = () => {
if (isLimitedMode.value) {
saveLimited();
} else {
submitForm('draft');
}
};
/** 审批完成后的保存逻辑 */
const saveLimited = () => {
projectPlanFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return;
buttonLoading.value = true;
try {
if (form.value.planStageList) {
form.value.planStageList.forEach(stage => {
stage.projectId = form.value.projectId;
});
}
await updateErpProjectPlan(form.value);
proxy?.$modal.msgSuccess('保存成功');
proxy?.$tab.closePage();
router.go(-1);
} finally {
buttonLoading.value = false;
}
});
};
/** 返回列表 */
const goBack = () => {
router.back();
};
/** 初始化数据 */
const initData = async () => {
await Promise.all([getProjectInfoList(), getUserList(), getContractInfoList()]);
syncRouteParams();
const projectPlanId = routeParams.value.projectPlanId;
if (projectPlanId && projectPlanId !== '0') {
const res = await getErpProjectPlan(projectPlanId as string);
form.value = res.data;
// 确保flowStatus有默认值用于approvalButton组件显示
if (!form.value.flowStatus) {
form.value.flowStatus = 'draft';
}
// 获取合同总价用于计算回款金额
if (form.value.contractId) {
const contract = contractInfoList.value.find(c => c.contractId === form.value.contractId);
if (contract) {
contractTotalPrice.value = contract.totalPrice || 0;
}
}
}
};
// 审批
const approvalVerifyOpen = async () => {
await submitVerifyRef.value?.openDialog(routeParams.value.taskId);
};
// 审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value?.init(form.value.projectPlanId);
};
// 提交回调
const submitCallback = async () => {
await proxy?.$tab.closePage(route);
router.go(-1);
};
onMounted(() => {
initData();
});
</script>
<style scoped lang="scss">
.el-divider {
margin: 20px 0;
}
</style>