|
|
<template>
|
|
|
<div class="p-2" v-loading="pageLoading">
|
|
|
<approvalButton
|
|
|
class="mb-3"
|
|
|
@submitForm="submitForm"
|
|
|
@approvalVerifyOpen="approvalVerifyOpen"
|
|
|
@handleApprovalRecord="handleApprovalRecord"
|
|
|
:buttonLoading="buttonLoading"
|
|
|
:id="form.tempTaskId as any"
|
|
|
:status="form.flowStatus as any"
|
|
|
:pageType="pageType"
|
|
|
:mode="false"
|
|
|
>
|
|
|
<el-button v-if="canSubmitFinish" type="success" icon="CircleCheck" @click="handleSubmitFinish" v-hasPermi="['oa/erp:tempTask:submitFinish']">
|
|
|
提交完成
|
|
|
</el-button>
|
|
|
<el-button v-if="canScoreClose" type="warning" icon="Star" @click="openScoreDialog" v-hasPermi="['oa/erp:tempTask:list']">
|
|
|
评分关闭
|
|
|
</el-button>
|
|
|
</approvalButton>
|
|
|
|
|
|
<el-card shadow="never" class="mb-3">
|
|
|
<el-form ref="tempTaskFormRef" :model="form" :rules="rules" label-width="120px" :disabled="isFormReadOnly">
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="任务编号" prop="tempTaskCode">
|
|
|
<el-input v-model="form.tempTaskCode" placeholder="后端自动生成" disabled />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="业务状态" prop="taskStatus">
|
|
|
<el-select v-model="form.taskStatus" disabled>
|
|
|
<el-option v-for="dict in temp_task_status" :key="dict.value" :label="dict.label" :value="dict.value" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="累计工时" prop="totalHours">
|
|
|
<el-input-number v-model="form.totalHours" :min="0" :precision="2" disabled controls-position="right" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="任务类型" prop="taskType">
|
|
|
<el-select v-model="form.taskType" placeholder="请选择任务类型" clearable>
|
|
|
<el-option v-for="dict in temp_task_type" :key="dict.value" :label="dict.label" :value="dict.value" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-form-item label="任务标题" prop="taskTitle">
|
|
|
<el-input v-model="form.taskTitle" placeholder="请输入任务标题" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="24">
|
|
|
<el-form-item label="任务描述" prop="taskDesc">
|
|
|
<el-input v-model="form.taskDesc" type="textarea" :rows="4" placeholder="请输入临时任务描述" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="计划开始" prop="planStartTime">
|
|
|
<el-date-picker v-model="form.planStartTime" type="date" value-format="YYYY-MM-DD" clearable placeholder="请选择计划开始" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="计划完成" prop="planEndTime">
|
|
|
<el-date-picker v-model="form.planEndTime" type="date" value-format="YYYY-MM-DD" clearable placeholder="请选择计划完成" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="预计工时" prop="estimateWorkload">
|
|
|
<el-input-number v-model="form.estimateWorkload" :min="0" :step="0.5" :precision="1" controls-position="right" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12" v-if="form.taskType === '3'">
|
|
|
<el-form-item label="主报工项目" prop="projectCode">
|
|
|
<el-input v-model="form.projectCode" placeholder="请选择项目" readonly>
|
|
|
<template #suffix>
|
|
|
<el-icon class="cursor-pointer" @click="openProjectSelect"><Search /></el-icon>
|
|
|
</template>
|
|
|
</el-input>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12" v-if="form.taskType === '3'">
|
|
|
<el-form-item label="项目名称" prop="projectName">
|
|
|
<el-input v-model="form.projectName" disabled placeholder="选择项目后自动填充" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12" v-if="form.taskType === '1'">
|
|
|
<el-form-item label="归集部门" prop="deptId">
|
|
|
<el-select v-model="form.deptId" placeholder="请选择归集部门" clearable filterable @change="handleTaskDeptChange">
|
|
|
<el-option v-for="dept in deptList" :key="dept.deptId" :label="dept.deptName" :value="dept.deptId" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12" v-if="form.taskType === '1'">
|
|
|
<el-form-item label="归集部门名称" prop="deptName">
|
|
|
<el-input v-model="form.deptName" disabled placeholder="选择部门后自动填充" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="发起人" prop="requesterName">
|
|
|
<el-input v-model="form.requesterName" disabled placeholder="后端按当前用户回填" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="实际需求人" prop="realRequesterName">
|
|
|
<el-input v-model="form.realRequesterName" readonly placeholder="默认可为空,代发起时选择">
|
|
|
<template #suffix>
|
|
|
<el-icon class="cursor-pointer" @click="openUserSelect('realRequester')"><Search /></el-icon>
|
|
|
</template>
|
|
|
</el-input>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="实际需求部门" prop="realRequestDeptId">
|
|
|
<el-select v-model="form.realRequestDeptId" placeholder="请选择实际需求部门" clearable filterable @change="handleRealDeptChange">
|
|
|
<el-option v-for="dept in deptList" :key="dept.deptId" :label="dept.deptName" :value="dept.deptId" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="软件部领导" prop="softwareLeaderName">
|
|
|
<el-input v-model="form.softwareLeaderName" readonly placeholder="请选择软件部领导">
|
|
|
<template #suffix>
|
|
|
<el-icon class="cursor-pointer" @click="openUserSelect('leader')"><Search /></el-icon>
|
|
|
</template>
|
|
|
</el-input>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="主执行人" prop="assigneeName">
|
|
|
<el-input v-model="form.assigneeName" readonly placeholder="请选择主执行人">
|
|
|
<template #suffix>
|
|
|
<el-icon class="cursor-pointer" @click="openUserSelect('assignee')"><Search /></el-icon>
|
|
|
</template>
|
|
|
</el-input>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<!-- Why actualStartTime 始终 disabled:由后端流程引擎在 leader_review 通过后自动落库,前端不可手动编辑 -->
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="实际开始" prop="actualStartTime">
|
|
|
<el-date-picker v-model="form.actualStartTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" disabled placeholder="流程自动记录" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="24">
|
|
|
<el-form-item label="备注" prop="remark">
|
|
|
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</el-form>
|
|
|
</el-card>
|
|
|
|
|
|
<el-row v-if="form.tempTaskId" :gutter="12">
|
|
|
<el-col :span="8">
|
|
|
<el-card shadow="never" class="mb-3">
|
|
|
<template #header>
|
|
|
<div class="flex justify-between items-center">
|
|
|
<span>参与人</span>
|
|
|
<el-button
|
|
|
v-if="canManageMembers"
|
|
|
type="primary"
|
|
|
plain
|
|
|
icon="Plus"
|
|
|
size="small"
|
|
|
@click="openMemberDialog"
|
|
|
v-hasPermi="['oa/erp:tempTask:list']"
|
|
|
>
|
|
|
协作人
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
<el-table :data="memberList" border>
|
|
|
<el-table-column label="类型" prop="memberType" width="100" align="center">
|
|
|
<template #default="scope">
|
|
|
<dict-tag :options="temp_task_member_type" :value="scope.row.memberType" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="姓名" prop="userName" min-width="100" align="center" />
|
|
|
<el-table-column label="协作说明" prop="joinRemark" min-width="150" show-overflow-tooltip align="center" />
|
|
|
<el-table-column v-if="canManageMembers" label="操作" width="80" align="center">
|
|
|
<template #default="scope">
|
|
|
<el-button v-if="scope.row.memberType === '2'" link type="danger" @click="handleRemoveMember(scope.row.memberId)">移除</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="16">
|
|
|
<el-card shadow="never" class="mb-3">
|
|
|
<template #header>
|
|
|
<div class="flex justify-between items-center">
|
|
|
<span>工时明细</span>
|
|
|
<el-button
|
|
|
v-if="canEditWorklog"
|
|
|
type="primary"
|
|
|
plain
|
|
|
icon="Plus"
|
|
|
size="small"
|
|
|
@click="openWorklogDialog()"
|
|
|
v-hasPermi="['oa/erp:tempTask:list']"
|
|
|
>
|
|
|
填报工时
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
<el-table :data="worklogList" border>
|
|
|
<el-table-column label="填报人" prop="userName" width="100" align="center" />
|
|
|
<el-table-column label="工作日期" prop="workDate" width="120" align="center">
|
|
|
<template #default="scope">{{ parseDate(scope.row.workDate) }}</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="自然周" min-width="190" align="center">
|
|
|
<template #default="scope">{{ parseDate(scope.row.weekStart) }} ~ {{ parseDate(scope.row.weekEnd) }}</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="工时" prop="hours" width="80" align="center" />
|
|
|
<el-table-column label="事项描述" prop="workContent" min-width="180" show-overflow-tooltip align="center" />
|
|
|
<el-table-column label="附件" min-width="160" align="center">
|
|
|
<template #default="scope">
|
|
|
<OssFilePreview v-if="scope.row.ossId" :oss-ids="scope.row.ossId" thumb-size="40px" />
|
|
|
<span v-else>暂无附件</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="状态" prop="lockFlag" width="90" align="center">
|
|
|
<template #default="scope">
|
|
|
<el-tag :type="scope.row.lockFlag === '1' ? 'info' : 'success'">{{ scope.row.lockFlag === '1' ? '已锁定' : '可编辑' }}</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column v-if="canEditWorklog" label="操作" width="110" align="center">
|
|
|
<template #default="scope">
|
|
|
<el-button v-if="scope.row.lockFlag !== '1'" link type="primary" @click="openWorklogDialog(scope.row)">编辑</el-button>
|
|
|
<el-button v-if="scope.row.lockFlag !== '1'" link type="danger" @click="handleDeleteWorklog(scope.row.worklogId)">删除</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-card v-if="form.tempTaskId" shadow="never">
|
|
|
<template #header>
|
|
|
<div class="flex justify-between items-center">
|
|
|
<span>评分结果</span>
|
|
|
<el-button
|
|
|
v-if="canScoreClose"
|
|
|
type="warning"
|
|
|
plain
|
|
|
icon="Star"
|
|
|
size="small"
|
|
|
@click="openScoreDialog"
|
|
|
v-hasPermi="['oa/erp:tempTask:list']"
|
|
|
>
|
|
|
评分关闭
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
<el-table :data="scoreList" border>
|
|
|
<el-table-column label="被评分人" prop="userName" width="120" align="center" />
|
|
|
<el-table-column label="评分等级" prop="scoreGrade" width="140" align="center">
|
|
|
<template #default="scope">
|
|
|
<dict-tag :options="temp_task_score" :value="scope.row.scoreGrade" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="评分说明" prop="scoreRemark" min-width="220" show-overflow-tooltip align="center" />
|
|
|
<el-table-column label="评分时间" prop="scoreTime" width="160" align="center">
|
|
|
<template #default="scope">{{ parseDateTime(scope.row.scoreTime) }}</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</el-card>
|
|
|
|
|
|
<el-dialog v-model="memberDialog.visible" title="新增协作人" width="620px" append-to-body>
|
|
|
<el-form ref="memberFormRef" :model="memberForm" :rules="memberRules" label-width="120px">
|
|
|
<el-form-item label="协作人" prop="userName">
|
|
|
<el-input v-model="memberForm.userName" readonly placeholder="请选择协作人">
|
|
|
<template #suffix>
|
|
|
<el-icon class="cursor-pointer" @click="openUserSelect('member')"><Search /></el-icon>
|
|
|
</template>
|
|
|
</el-input>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="协作说明" prop="joinRemark">
|
|
|
<el-input v-model="memberForm.joinRemark" type="textarea" :rows="3" placeholder="请输入协作事项说明" />
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
<el-button @click="memberDialog.visible = false">取消</el-button>
|
|
|
<el-button type="primary" :loading="buttonLoading" @click="handleAddMember">确定</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
<el-dialog v-model="worklogDialog.visible" title="工时明细" width="720px" append-to-body>
|
|
|
<el-form ref="worklogFormRef" :model="worklogForm" :rules="worklogRules" label-width="120px">
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="工作日期" prop="workDate">
|
|
|
<el-date-picker v-model="worklogForm.workDate" type="date" value-format="YYYY-MM-DD" clearable />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="工时" prop="hours">
|
|
|
<el-input-number v-model="worklogForm.hours" :min="0.5" :step="0.5" :precision="1" controls-position="right" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="24">
|
|
|
<el-form-item label="事项描述" prop="workContent">
|
|
|
<el-input v-model="worklogForm.workContent" type="textarea" :rows="4" placeholder="请输入本次工作内容" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="24">
|
|
|
<el-form-item label="附件" prop="ossId">
|
|
|
<FileUpload
|
|
|
v-model="worklogOssIdString"
|
|
|
:limit="5"
|
|
|
:file-size="20"
|
|
|
:file-type="['png', 'jpg', 'jpeg', 'doc', 'docx', 'pdf', 'xls', 'xlsx', 'txt']"
|
|
|
:is-show-tip="true"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
<el-button @click="worklogDialog.visible = false">取消</el-button>
|
|
|
<el-button type="primary" :loading="buttonLoading" @click="handleSaveWorklog">保存</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
<el-dialog v-model="scoreDialog.visible" title="领导评分关闭" width="820px" append-to-body>
|
|
|
<el-form ref="scoreFormRef" :model="scoreForm" label-width="120px">
|
|
|
<el-table :data="scoreForm.scoreList" border>
|
|
|
<el-table-column label="人员" min-width="140" align="center">
|
|
|
<template #default="scope">
|
|
|
{{ memberNameMap[scope.row.memberId] || scope.row.userId }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="评分等级" width="180" align="center">
|
|
|
<template #default="scope">
|
|
|
<el-select v-model="scope.row.scoreGrade" placeholder="请选择评分">
|
|
|
<el-option v-for="dict in temp_task_score" :key="dict.value" :label="dict.label" :value="dict.value" />
|
|
|
</el-select>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="评分说明" min-width="240" align="center">
|
|
|
<template #default="scope">
|
|
|
<el-input v-model="scope.row.scoreRemark" placeholder="请输入评分说明" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
<el-form-item class="mt-3" label="最终审核意见">
|
|
|
<el-input v-model="scoreForm.leaderFinalOpinion" type="textarea" :rows="3" placeholder="请输入最终审核意见" />
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
<el-button @click="scoreDialog.visible = false">取消</el-button>
|
|
|
<el-button type="primary" :loading="buttonLoading" @click="handleScoreAndClose">评分关闭</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
|
|
|
<approvalRecord ref="approvalRecordRef" />
|
|
|
<ProjectSelect ref="projectSelectRef" :multiple="false" @confirm-call-back="handleProjectSelect" />
|
|
|
<UserSelect ref="userSelectRef" :multiple="userSelectMode === 'cc'" @confirm-call-back="handleUserSelect" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup name="TempTaskEdit" lang="ts">
|
|
|
import {
|
|
|
addTempTask,
|
|
|
addTempTaskMember,
|
|
|
assigneeReviewTempTask,
|
|
|
delTempTaskMember,
|
|
|
delTempTaskWorklog,
|
|
|
getTempTask,
|
|
|
leaderReviewAndCompleteTempTask,
|
|
|
listTempTaskMember,
|
|
|
listTempTaskScore,
|
|
|
listTempTaskWorklog,
|
|
|
saveTempTaskWorklog,
|
|
|
scoreAndCloseTempTask,
|
|
|
submitFinishTempTask,
|
|
|
tempTaskSubmitAndFlowStart,
|
|
|
updateTempTask,
|
|
|
syncTaskState
|
|
|
} from '@/api/oa/erp/tempTask';
|
|
|
import type {
|
|
|
TempTaskForm,
|
|
|
TempTaskMemberForm,
|
|
|
TempTaskMemberVO,
|
|
|
TempTaskScoreSubmitForm,
|
|
|
TempTaskScoreVO,
|
|
|
TempTaskWorklogForm,
|
|
|
TempTaskWorklogVO
|
|
|
} from '@/api/oa/erp/tempTask/types';
|
|
|
import type { ProjectInfoVO } from '@/api/oa/erp/projectInfo/types';
|
|
|
import type { DeptVO } from '@/api/system/dept/types';
|
|
|
import { allListDept } from '@/api/system/dept';
|
|
|
import { getTask } from '@/api/workflow/task';
|
|
|
import { FlowCodeEnum } from '@/enums/OAEnum';
|
|
|
import ApprovalButton from '@/components/Process/approvalButton.vue';
|
|
|
import SubmitVerify from '@/components/Process/submitVerify.vue';
|
|
|
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
|
|
|
import ProjectSelect from '@/components/ProjectSelect/index.vue';
|
|
|
import UserSelect from '@/components/UserSelect/index.vue';
|
|
|
import FileUpload from '@/components/FileUpload/index.vue';
|
|
|
import OssFilePreview from '@/components/OssFilePreview/index.vue';
|
|
|
|
|
|
type UserSelectMode = 'leader' | 'assignee' | 'realRequester' | 'member' | 'cc';
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as any;
|
|
|
const route = useRoute();
|
|
|
const router = useRouter();
|
|
|
|
|
|
const { temp_task_type, temp_task_status, temp_task_member_type, temp_task_score } = toRefs<any>(
|
|
|
proxy?.useDict('temp_task_type', 'temp_task_status', 'temp_task_member_type', 'temp_task_score')
|
|
|
);
|
|
|
|
|
|
const pageLoading = ref(false);
|
|
|
const buttonLoading = ref(false);
|
|
|
const pageType = ref<string>((route.query.type as string) || 'add');
|
|
|
const tempTaskFormRef = ref<ElFormInstance>();
|
|
|
const memberFormRef = ref<ElFormInstance>();
|
|
|
const worklogFormRef = ref<ElFormInstance>();
|
|
|
const scoreFormRef = ref<ElFormInstance>();
|
|
|
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
|
|
|
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
|
|
|
const projectSelectRef = ref<InstanceType<typeof ProjectSelect>>();
|
|
|
const userSelectRef = ref<InstanceType<typeof UserSelect>>();
|
|
|
|
|
|
const userSelectMode = ref<UserSelectMode>('leader');
|
|
|
const taskVariables = ref<Record<string, unknown>>({});
|
|
|
const currentNodeCode = ref<string>('');
|
|
|
const deptList = ref<DeptVO[]>([]);
|
|
|
const memberList = ref<TempTaskMemberVO[]>([]);
|
|
|
const worklogList = ref<TempTaskWorklogVO[]>([]);
|
|
|
const scoreList = ref<TempTaskScoreVO[]>([]);
|
|
|
|
|
|
const initFormData: TempTaskForm = {
|
|
|
tempTaskId: undefined,
|
|
|
taskId: undefined,
|
|
|
tempTaskCode: undefined,
|
|
|
taskTitle: undefined,
|
|
|
taskDesc: undefined,
|
|
|
taskType: '1',
|
|
|
planStartTime: undefined,
|
|
|
planEndTime: undefined,
|
|
|
actualStartTime: undefined,
|
|
|
actualFinishTime: undefined,
|
|
|
projectId: undefined,
|
|
|
projectCode: undefined,
|
|
|
projectName: undefined,
|
|
|
deptId: undefined,
|
|
|
deptName: undefined,
|
|
|
requesterId: undefined,
|
|
|
requesterName: undefined,
|
|
|
realRequesterId: undefined,
|
|
|
realRequesterName: undefined,
|
|
|
realRequestDeptId: undefined,
|
|
|
realRequestDeptName: undefined,
|
|
|
softwareLeaderId: undefined,
|
|
|
softwareLeaderName: undefined,
|
|
|
assigneeId: undefined,
|
|
|
assigneeName: undefined,
|
|
|
estimateWorkload: undefined,
|
|
|
totalHours: undefined,
|
|
|
taskStatus: '1',
|
|
|
flowStatus: 'draft',
|
|
|
ccUserIds: undefined,
|
|
|
remark: undefined,
|
|
|
flowCode: FlowCodeEnum.TEMP_TASK_CODE,
|
|
|
variables: {},
|
|
|
bizExt: {}
|
|
|
};
|
|
|
|
|
|
const form = ref<TempTaskForm>({ ...initFormData });
|
|
|
|
|
|
const rules: ElFormRules = {
|
|
|
taskDesc: [{ required: true, message: '任务描述不能为空', trigger: 'blur' }],
|
|
|
taskType: [{ required: true, message: '任务类型不能为空', trigger: 'change' }],
|
|
|
softwareLeaderName: [{ required: true, message: '软件部领导不能为空', trigger: 'change' }],
|
|
|
projectCode: [
|
|
|
{
|
|
|
validator: (_rule, value, callback) => {
|
|
|
if (form.value.taskType === '3' && !value) {
|
|
|
callback(new Error('项目任务必须选择项目'));
|
|
|
} else {
|
|
|
callback();
|
|
|
}
|
|
|
},
|
|
|
trigger: 'change'
|
|
|
}
|
|
|
],
|
|
|
deptId: [
|
|
|
{
|
|
|
validator: (_rule, value, callback) => {
|
|
|
if (form.value.taskType === '1' && !value) {
|
|
|
callback(new Error('部门任务必须选择归集部门'));
|
|
|
} else {
|
|
|
callback();
|
|
|
}
|
|
|
},
|
|
|
trigger: 'change'
|
|
|
}
|
|
|
]
|
|
|
};
|
|
|
|
|
|
const memberDialog = reactive({ visible: false });
|
|
|
const memberForm = ref<TempTaskMemberForm>({});
|
|
|
const memberRules: ElFormRules = {
|
|
|
userName: [{ required: true, message: '协作人不能为空', trigger: 'change' }]
|
|
|
};
|
|
|
|
|
|
const worklogDialog = reactive({ visible: false });
|
|
|
const worklogForm = ref<TempTaskWorklogForm>({});
|
|
|
const worklogRules: ElFormRules = {
|
|
|
workDate: [{ required: true, message: '工作日期不能为空', trigger: 'change' }],
|
|
|
hours: [{ required: true, message: '工时不能为空', trigger: 'blur' }],
|
|
|
workContent: [{ required: true, message: '事项描述不能为空', trigger: 'blur' }]
|
|
|
};
|
|
|
|
|
|
const scoreDialog = reactive({ visible: false });
|
|
|
const scoreForm = ref<TempTaskScoreSubmitForm>({
|
|
|
tempTaskId: '',
|
|
|
taskId: '',
|
|
|
leaderFinalOpinion: undefined,
|
|
|
scoreList: []
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* 表单是否只读。
|
|
|
*
|
|
|
* 规则:
|
|
|
* - 查看模式(pageType=view) -> 全部只读
|
|
|
* - 审批模式(pageType=approval) + 当前节点不是 leader_review/assignee_review -> 只读
|
|
|
* - 审批模式 + 当前节点是 leader_review -> 可编辑(领导审批时可修改主执行人、软件部领导等字段)
|
|
|
* - 审批模式 + 当前节点是 assignee_review -> 可编辑(主执行人审阅时可修改标题/描述/计划周期/预估工时)
|
|
|
*
|
|
|
* 为什么 leader_review 和 assignee_review 在审批模式也可编辑:
|
|
|
* 这两个节点的业务语义是"审批者可以修正业务数据",而非纯粹的"只读审批"。
|
|
|
* 审批即修改,所以表单在这些节点保持可编辑状态。
|
|
|
*/
|
|
|
// Why:type=execute 模式主表只读,参与人/工时明细单独控制;
|
|
|
// leader_review/assignee_review 节点审批人可修正业务数据;
|
|
|
// execute 工作流节点同样保持可编辑。
|
|
|
const isFormReadOnly = computed(
|
|
|
() => pageType.value === 'view'
|
|
|
|| pageType.value === 'execute'
|
|
|
|| (pageType.value === 'approval' && !['leader_review', 'assignee_review', 'execute'].includes(currentNodeCode.value))
|
|
|
);
|
|
|
/**
|
|
|
* 是否允许管理协作人(新增/移除)。
|
|
|
* type=execute 模式 + 执行中(taskStatus=3) 时可操作。
|
|
|
*/
|
|
|
const canManageMembers = computed(() => !!form.value.tempTaskId && pageType.value !== 'view' && form.value.taskStatus === '3');
|
|
|
/**
|
|
|
* 是否允许编辑工时明细(填报/修改/删除)。
|
|
|
* 规则同上。
|
|
|
*/
|
|
|
const canEditWorklog = computed(() => !!form.value.tempTaskId && pageType.value !== 'view' && form.value.taskStatus === '3');
|
|
|
/**
|
|
|
* 是否允许"提交完成"按钮显示。
|
|
|
*
|
|
|
* 需要同时满足五个条件:
|
|
|
* 1. tempTaskId 存在(任务已保存,非新增草稿)
|
|
|
* 2. route.query.taskId 存在(从工作流待办进入,非直接编辑进入)
|
|
|
* -- 这是为了保证 taskId 存在,提交完成后端需要它来 completeTask
|
|
|
* 3. 非查看模式
|
|
|
* 4. taskStatus === '3'(执行中)
|
|
|
* 5. currentNodeCode === 'execute'(当前流程节点是执行节点)
|
|
|
* -- 只有主执行人在 execute 节点时才能提交完成,防止其他角色误操作
|
|
|
*/
|
|
|
const canSubmitFinish = computed(
|
|
|
() =>
|
|
|
!!form.value.tempTaskId &&
|
|
|
!!route.query.taskId &&
|
|
|
pageType.value !== 'view' &&
|
|
|
form.value.taskStatus === '3' &&
|
|
|
currentNodeCode.value === 'execute'
|
|
|
);
|
|
|
/**
|
|
|
* 是否允许"评分关闭"按钮显示。
|
|
|
*
|
|
|
* 需要同时满足五个条件:
|
|
|
* 1. tempTaskId 存在(任务已保存)
|
|
|
* 2. route.query.taskId 存在(从工作流待办进入)
|
|
|
* 3. 非查看模式
|
|
|
* 4. taskStatus === '4'(待领导审核 -- 主执行人已提交完成,等待领导评分)
|
|
|
* 5. currentNodeCode === 'leader_final'(当前流程节点是领导终审节点)
|
|
|
* -- 只有领导在 leader_final 节点时才能评分关闭,评分即关闭,不可逆
|
|
|
*/
|
|
|
const canScoreClose = computed(
|
|
|
() =>
|
|
|
!!form.value.tempTaskId &&
|
|
|
!!route.query.taskId &&
|
|
|
pageType.value !== 'view' &&
|
|
|
// Why:执行人可能绕过 submitFinish 直接用工作流审批按钮推进到 leader_final,
|
|
|
// 此时 taskStatus 仍为 3,但评分人已到 leader_final 节点,仍需允许评分关闭。
|
|
|
(form.value.taskStatus === '3' || form.value.taskStatus === '4') &&
|
|
|
currentNodeCode.value === 'leader_final'
|
|
|
);
|
|
|
|
|
|
const memberNameMap = computed<Record<string, string>>(() => {
|
|
|
const map: Record<string, string> = {};
|
|
|
memberList.value.forEach((member) => {
|
|
|
map[String(member.memberId)] = member.userName || '';
|
|
|
});
|
|
|
return map;
|
|
|
});
|
|
|
|
|
|
const worklogOssIdString = computed({
|
|
|
get() {
|
|
|
return worklogForm.value.ossId ? String(worklogForm.value.ossId) : '';
|
|
|
},
|
|
|
set(val: string) {
|
|
|
worklogForm.value.ossId = val || undefined;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
const parseDate = (value?: string) => {
|
|
|
return value ? String(value).slice(0, 10) : '';
|
|
|
};
|
|
|
|
|
|
const parseDateTime = (value?: string) => {
|
|
|
return value ? String(value).replace('T', ' ').slice(0, 16) : '';
|
|
|
};
|
|
|
|
|
|
const getDeptList = async () => {
|
|
|
const res = await allListDept({ status: 0 } as any);
|
|
|
deptList.value = res.data || [];
|
|
|
};
|
|
|
|
|
|
const findDept = (deptId?: string | number) => {
|
|
|
return deptList.value.find((dept) => dept.deptId === deptId);
|
|
|
};
|
|
|
|
|
|
const handleTaskDeptChange = (deptId?: string | number) => {
|
|
|
form.value.deptName = findDept(deptId)?.deptName;
|
|
|
};
|
|
|
|
|
|
const handleRealDeptChange = (deptId?: string | number) => {
|
|
|
form.value.realRequestDeptName = findDept(deptId)?.deptName;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 监听 taskType 切换,联动清除不适用的表单字段。
|
|
|
* - 切换到非项目类型(taskType !== '3'):清除项目相关字段,避免脏数据带入提交
|
|
|
* - 切换到非部门类型(taskType !== '1'):清除部门相关字段
|
|
|
* 此逻辑确保表单提交时不会携带与当前任务类型无关的字段值。
|
|
|
*/
|
|
|
watch(
|
|
|
() => form.value.taskType,
|
|
|
(taskType) => {
|
|
|
if (taskType !== '3') {
|
|
|
form.value.projectId = undefined;
|
|
|
form.value.projectCode = undefined;
|
|
|
form.value.projectName = undefined;
|
|
|
}
|
|
|
if (taskType !== '1') {
|
|
|
form.value.deptId = undefined;
|
|
|
form.value.deptName = undefined;
|
|
|
}
|
|
|
}
|
|
|
);
|
|
|
|
|
|
const openProjectSelect = () => {
|
|
|
if (isFormReadOnly.value) {
|
|
|
return;
|
|
|
}
|
|
|
projectSelectRef.value?.open();
|
|
|
};
|
|
|
|
|
|
const handleProjectSelect = (data: ProjectInfoVO[]) => {
|
|
|
const project = data?.[0];
|
|
|
if (!project) {
|
|
|
return;
|
|
|
}
|
|
|
form.value.projectId = project.projectId;
|
|
|
form.value.projectCode = project.projectCode;
|
|
|
form.value.projectName = project.projectName;
|
|
|
};
|
|
|
|
|
|
const openUserSelect = (mode: UserSelectMode) => {
|
|
|
if (isFormReadOnly.value && mode !== 'member') {
|
|
|
return;
|
|
|
}
|
|
|
userSelectMode.value = mode;
|
|
|
userSelectRef.value?.open();
|
|
|
};
|
|
|
|
|
|
const handleUserSelect = (users: Array<Record<string, any>>) => {
|
|
|
const selected = users?.[0];
|
|
|
if (!selected) {
|
|
|
return;
|
|
|
}
|
|
|
const userId = selected.userId;
|
|
|
const name = selected.nickName || selected.userName;
|
|
|
if (userSelectMode.value === 'leader') {
|
|
|
form.value.softwareLeaderId = userId;
|
|
|
form.value.softwareLeaderName = name;
|
|
|
} else if (userSelectMode.value === 'assignee') {
|
|
|
form.value.assigneeId = userId;
|
|
|
form.value.assigneeName = name;
|
|
|
} else if (userSelectMode.value === 'realRequester') {
|
|
|
form.value.realRequesterId = userId;
|
|
|
form.value.realRequesterName = name;
|
|
|
if (selected.deptId) {
|
|
|
form.value.realRequestDeptId = selected.deptId;
|
|
|
form.value.realRequestDeptName = selected.deptName;
|
|
|
}
|
|
|
} else if (userSelectMode.value === 'member') {
|
|
|
memberForm.value.userId = userId;
|
|
|
memberForm.value.userName = name;
|
|
|
memberForm.value.memberDeptId = selected.deptId;
|
|
|
} else {
|
|
|
form.value.ccUserIds = users.map((item) => item.userId).join(',');
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 从专用子表端点加载参与人/工时/评分列表(AD-13/AD-16)。
|
|
|
*
|
|
|
* 这些子表通过独立 API 端点查询,而非依赖主表 queryById 的嵌套返回。
|
|
|
* 为什么不用 loadDetail 中的嵌套数据而要再调一次:
|
|
|
* 主表 queryById 可能出于性能考虑不返回完整子表数据(不包含 members/worklogs/scores),
|
|
|
* 或者列表页查询不携带子表,只有编辑页才加载。loadSubLists 保证子表数据是最新状态。
|
|
|
*/
|
|
|
const loadSubLists = async () => {
|
|
|
if (!form.value.tempTaskId) {
|
|
|
memberList.value = [];
|
|
|
worklogList.value = [];
|
|
|
scoreList.value = [];
|
|
|
return;
|
|
|
}
|
|
|
const tempTaskId = form.value.tempTaskId;
|
|
|
const [memberRes, worklogRes, scoreRes] = await Promise.all([
|
|
|
listTempTaskMember(tempTaskId),
|
|
|
listTempTaskWorklog(tempTaskId),
|
|
|
listTempTaskScore(tempTaskId)
|
|
|
]);
|
|
|
memberList.value = memberRes.data || [];
|
|
|
worklogList.value = worklogRes.data || [];
|
|
|
scoreList.value = scoreRes.data || [];
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 加载任务详情。
|
|
|
*
|
|
|
* 数据来源:
|
|
|
* - 主表字段(form):来自 getTempTask(queryById),包含业务字段快照
|
|
|
* - 子表字段(members/worklogs/scores):先用 queryById 返回的嵌套数据做快速渲染,
|
|
|
* 然后调用 loadSubLists() 从专用端点拉取最新数据覆盖
|
|
|
*
|
|
|
* 为什么需要两步加载:
|
|
|
* queryById 可能返回子表的旧版本数据(如列表页用的同一个 VO),
|
|
|
* loadSubLists 从独立端点获取确保数据是最新的。
|
|
|
*/
|
|
|
const loadDetail = async (id: string | number) => {
|
|
|
const res = await getTempTask(id);
|
|
|
form.value = {
|
|
|
...initFormData,
|
|
|
...res.data,
|
|
|
flowCode: FlowCodeEnum.TEMP_TASK_CODE
|
|
|
};
|
|
|
// 先用嵌套数据快速渲染,避免空白闪烁
|
|
|
memberList.value = res.data?.members || [];
|
|
|
worklogList.value = res.data?.worklogs || [];
|
|
|
scoreList.value = res.data?.scores || [];
|
|
|
// 再从专用端点拉取最新子表数据覆盖
|
|
|
await loadSubLists();
|
|
|
};
|
|
|
|
|
|
const loadCurrentWorkflowTask = async () => {
|
|
|
const taskId = route.query.taskId as string | undefined;
|
|
|
if (!taskId) {
|
|
|
currentNodeCode.value = '';
|
|
|
return;
|
|
|
}
|
|
|
const res = await getTask(taskId);
|
|
|
currentNodeCode.value = res.data?.nodeCode || '';
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 组装工作流 variables 和 bizExt 载荷。
|
|
|
*
|
|
|
* variables 的来源:
|
|
|
* - has_assignee: 控制流程网关分支 -- 有主执行人时跳过 assignee_confirm 节点,直接进入执行
|
|
|
* - reassigned: 保留后端可能写入的换人标记,用于下一次提交时维持流程变量
|
|
|
* - 其余字段:从 form 中提取业务关键字段,用于 Warm-Flow 条件表达式(SpEL)计算路由/办理人
|
|
|
*
|
|
|
* null/undefined/空字符串清理:
|
|
|
* Warm-Flow 的 SpEL 表达式对 null 和空字符串处理不同,为保持一致性,
|
|
|
* 前端在提交前删除所有空值变量,让后端按默认逻辑处理缺失字段。
|
|
|
*
|
|
|
* bizExt 用于审批中心/待办列表渲染,截断到 80 字符防止展示溢出。
|
|
|
*/
|
|
|
const buildVariables = () => {
|
|
|
taskVariables.value = {
|
|
|
has_assignee: form.value.assigneeId ? '1' : '0',
|
|
|
// 保留后端可能写入的 reassigned 标记,避免下拉框变更后丢失换人状态
|
|
|
reassigned: (form.value.variables as Record<string, unknown>)?.reassigned || '0',
|
|
|
requesterId: form.value.requesterId,
|
|
|
realRequesterId: form.value.realRequesterId,
|
|
|
realRequestDeptId: form.value.realRequestDeptId,
|
|
|
softwareLeaderId: form.value.softwareLeaderId,
|
|
|
assigneeId: form.value.assigneeId,
|
|
|
ccUserIds: form.value.ccUserIds,
|
|
|
taskTitle: form.value.taskTitle,
|
|
|
taskDesc: form.value.taskDesc,
|
|
|
taskType: form.value.taskType,
|
|
|
projectId: form.value.projectId,
|
|
|
deptId: form.value.deptId,
|
|
|
planStartTime: form.value.planStartTime,
|
|
|
planEndTime: form.value.planEndTime,
|
|
|
estimateWorkload: form.value.estimateWorkload,
|
|
|
actualStartTime: form.value.actualStartTime,
|
|
|
assigneeOpinion: form.value.assigneeOpinion,
|
|
|
leaderOpinion: form.value.leaderOpinion,
|
|
|
leaderFinalOpinion: scoreForm.value.leaderFinalOpinion
|
|
|
};
|
|
|
// 清理空值:Warm-Flow 的 SpEL 表达式对 null/空串/undefined 处理行为不一致,
|
|
|
// 删除全部空值变量让后端按缺失字段的默认逻辑处理,避免条件分支误判
|
|
|
Object.keys(taskVariables.value).forEach((key) => {
|
|
|
if (taskVariables.value[key] === undefined || taskVariables.value[key] === null || taskVariables.value[key] === '') {
|
|
|
delete taskVariables.value[key];
|
|
|
}
|
|
|
});
|
|
|
form.value.variables = { ...taskVariables.value };
|
|
|
// 组装 bizExt:用于审批中心/待办列表展示业务信息
|
|
|
const businessTitle = `${form.value.tempTaskCode || '临时任务'}-${form.value.taskDesc || form.value.taskTitle || ''}`;
|
|
|
form.value.bizExt = {
|
|
|
businessId: form.value.tempTaskId,
|
|
|
businessCode: form.value.tempTaskCode,
|
|
|
businessTitle: businessTitle.slice(0, 80) // 截断防止审批中心展示溢出
|
|
|
};
|
|
|
};
|
|
|
|
|
|
const validateMainForm = async () => {
|
|
|
if (!tempTaskFormRef.value) {
|
|
|
return true;
|
|
|
}
|
|
|
return await tempTaskFormRef.value.validate().catch(() => false);
|
|
|
};
|
|
|
|
|
|
const submitForm = async (status: string) => {
|
|
|
const valid = await validateMainForm();
|
|
|
if (!valid) {
|
|
|
return;
|
|
|
}
|
|
|
buttonLoading.value = true;
|
|
|
try {
|
|
|
buildVariables();
|
|
|
if (status !== 'draft') {
|
|
|
const res = await tempTaskSubmitAndFlowStart(form.value);
|
|
|
form.value = { ...initFormData, ...res.data, flowCode: FlowCodeEnum.TEMP_TASK_CODE };
|
|
|
proxy?.$modal.msgSuccess('操作成功');
|
|
|
} else {
|
|
|
form.value.taskStatus = '1';
|
|
|
form.value.flowStatus = 'draft';
|
|
|
if (form.value.tempTaskId) {
|
|
|
await updateTempTask(form.value);
|
|
|
} else {
|
|
|
await addTempTask(form.value);
|
|
|
}
|
|
|
proxy?.$modal.msgSuccess('暂存成功');
|
|
|
}
|
|
|
proxy?.$tab.closePage(route);
|
|
|
router.go(-1);
|
|
|
} finally {
|
|
|
buttonLoading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const openMemberDialog = () => {
|
|
|
memberForm.value = {
|
|
|
tempTaskId: form.value.tempTaskId,
|
|
|
memberType: '2'
|
|
|
};
|
|
|
memberDialog.visible = true;
|
|
|
};
|
|
|
|
|
|
const handleAddMember = async () => {
|
|
|
const valid = await memberFormRef.value?.validate().catch(() => false);
|
|
|
if (!valid) {
|
|
|
return;
|
|
|
}
|
|
|
buttonLoading.value = true;
|
|
|
try {
|
|
|
await addTempTaskMember(memberForm.value);
|
|
|
proxy?.$modal.msgSuccess('协作人已添加');
|
|
|
memberDialog.visible = false;
|
|
|
await loadSubLists();
|
|
|
} finally {
|
|
|
buttonLoading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const handleRemoveMember = async (memberId: string | number) => {
|
|
|
await proxy?.$modal.confirm('确认移除该协作人?');
|
|
|
await delTempTaskMember(memberId);
|
|
|
proxy?.$modal.msgSuccess('移除成功');
|
|
|
await loadSubLists();
|
|
|
};
|
|
|
|
|
|
const openWorklogDialog = (row?: TempTaskWorklogVO) => {
|
|
|
worklogForm.value = row
|
|
|
? { ...row }
|
|
|
: {
|
|
|
tempTaskId: form.value.tempTaskId,
|
|
|
workDate: undefined,
|
|
|
hours: 0.5,
|
|
|
workContent: undefined,
|
|
|
ossId: undefined
|
|
|
};
|
|
|
worklogDialog.visible = true;
|
|
|
};
|
|
|
|
|
|
const handleSaveWorklog = async () => {
|
|
|
const valid = await worklogFormRef.value?.validate().catch(() => false);
|
|
|
if (!valid) {
|
|
|
return;
|
|
|
}
|
|
|
buttonLoading.value = true;
|
|
|
try {
|
|
|
await saveTempTaskWorklog(worklogForm.value);
|
|
|
proxy?.$modal.msgSuccess('工时明细已保存');
|
|
|
worklogDialog.visible = false;
|
|
|
await loadSubLists();
|
|
|
} finally {
|
|
|
buttonLoading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const handleDeleteWorklog = async (worklogId: string | number) => {
|
|
|
await proxy?.$modal.confirm('确认删除该工时明细?');
|
|
|
await delTempTaskWorklog(worklogId);
|
|
|
proxy?.$modal.msgSuccess('删除成功');
|
|
|
await loadSubLists();
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 主执行人提交完成。
|
|
|
*
|
|
|
* 前置校验:
|
|
|
* 1. tempTaskId 和 taskId 必须存在(从工作流待办进入才有 taskId)
|
|
|
* 2. 当前流程节点必须是 execute(防止在非执行节点误操作)
|
|
|
*
|
|
|
* 提交后行为:
|
|
|
* - 后端锁定全部工时明细(lockFlag=1)
|
|
|
* - 累计汇总 totalHours
|
|
|
* - 业务状态从 3(执行中) 扭转为 4(待领导审核)
|
|
|
* - 流程从 execute 流转到 leader_final
|
|
|
* - 关闭当前页返回列表
|
|
|
*/
|
|
|
const handleSubmitFinish = async () => {
|
|
|
const taskId = route.query.taskId as string | undefined;
|
|
|
if (!form.value.tempTaskId || !taskId) {
|
|
|
proxy?.$modal.msgWarning('请从执行待办进入后提交完成');
|
|
|
return;
|
|
|
}
|
|
|
// 双保险:再次获取最新节点状态,防止 stale closure
|
|
|
if (!currentNodeCode.value) {
|
|
|
await loadCurrentWorkflowTask();
|
|
|
}
|
|
|
if (currentNodeCode.value !== 'execute') {
|
|
|
proxy?.$modal.msgWarning('当前流程节点不是执行节点,不能提交完成');
|
|
|
return;
|
|
|
}
|
|
|
await proxy?.$modal.confirm('确认提交完成并锁定全部工时明细?');
|
|
|
buttonLoading.value = true;
|
|
|
try {
|
|
|
await submitFinishTempTask({
|
|
|
tempTaskId: form.value.tempTaskId,
|
|
|
taskId
|
|
|
});
|
|
|
proxy?.$modal.msgSuccess('提交完成成功');
|
|
|
proxy?.$tab.closePage(route);
|
|
|
router.go(-1);
|
|
|
} finally {
|
|
|
buttonLoading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 打开评分关闭对话框。
|
|
|
*
|
|
|
* 预填充逻辑(复用已有评分):
|
|
|
* - 遍历所有参与人(memberList),为每个参与人创建一条评分表单
|
|
|
* - 如果后端已有评分数据(例如领导之前打开过评分弹窗但未提交),
|
|
|
* 则用已有评分预填充 scoreGrade 和 scoreRemark,
|
|
|
* 方便领导再次打开时不丢失之前填写的评分内容
|
|
|
* - 为什么需要这个预填充:评分关闭是一次性操作,但领导可能多次打开弹窗查看/调整,
|
|
|
* 已有的评分数据不应在重新打开时丢失
|
|
|
*/
|
|
|
const openScoreDialog = () => {
|
|
|
const taskId = route.query.taskId as string | undefined;
|
|
|
if (!form.value.tempTaskId || !taskId) {
|
|
|
return;
|
|
|
}
|
|
|
scoreForm.value = {
|
|
|
tempTaskId: form.value.tempTaskId,
|
|
|
taskId,
|
|
|
leaderFinalOpinion: undefined,
|
|
|
scoreList: memberList.value.map((member) => {
|
|
|
const oldScore = scoreList.value.find((score) => String(score.memberId) === String(member.memberId));
|
|
|
return {
|
|
|
memberId: member.memberId,
|
|
|
userId: member.userId,
|
|
|
// 复用已有评分作为预填充值,防止重新打开弹窗时评分数据丢失
|
|
|
scoreGrade: oldScore?.scoreGrade || '',
|
|
|
scoreRemark: oldScore?.scoreRemark
|
|
|
};
|
|
|
})
|
|
|
};
|
|
|
scoreDialog.visible = true;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 领导评分关闭提交。
|
|
|
*
|
|
|
* 校验:所有参与人的评分等级必须填写,不能有遗漏。
|
|
|
* 提交后行为:
|
|
|
* - 后端逐人写评分 + 落库 actualFinishTime + 关闭任务(taskStatus=5)
|
|
|
* - 完成 leader_final 工作流节点
|
|
|
* - 关闭页面返回列表
|
|
|
*/
|
|
|
const handleScoreAndClose = async () => {
|
|
|
// 确保所有参与人的评分等级都已选择,防止部分人员漏评
|
|
|
if (scoreForm.value.scoreList.some((item) => !item.scoreGrade)) {
|
|
|
proxy?.$modal.msgWarning('请为全部参与人选择评分等级');
|
|
|
return;
|
|
|
}
|
|
|
buttonLoading.value = true;
|
|
|
try {
|
|
|
await scoreAndCloseTempTask(scoreForm.value);
|
|
|
proxy?.$modal.msgSuccess('评分关闭成功');
|
|
|
scoreDialog.visible = false;
|
|
|
proxy?.$tab.closePage(route);
|
|
|
router.go(-1);
|
|
|
} finally {
|
|
|
buttonLoading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 审批按钮回调,根据当前工作流节点做三路分发。
|
|
|
*
|
|
|
* 分支逻辑:
|
|
|
* 1. leader_review(领导审批节点):
|
|
|
* - 先校验主表单
|
|
|
* - 调用 leaderReviewAndCompleteTempTask 原子完成落库+流转
|
|
|
* - 直接关闭页面返回列表(无需弹出 submitVerify,因为后端已完成 completeTask)
|
|
|
*
|
|
|
* 2. assignee_review(主执行人审阅节点):
|
|
|
* - 调用 assigneeReviewTempTask 回写审阅可改字段
|
|
|
* - 重新 buildVariables() 装配最新的流程变量
|
|
|
* - 弹出 submitVerify 对话框,由用户确认后调用 workflow completeTask 流转
|
|
|
*
|
|
|
* 3. 其他节点(新增/修改提交、驳回重提等):
|
|
|
* - 直接 buildVariables() 装配流程变量
|
|
|
* - 弹出 submitVerify 对话框,走标准提交流程
|
|
|
*
|
|
|
* 为什么 leader_review 不弹出 submitVerify:
|
|
|
* leaderReviewAndCompleteTempTask 已经在后端原子完成了 completeTask,
|
|
|
* 如果前端再调 completeTask 会导致重复流转。
|
|
|
*/
|
|
|
const approvalVerifyOpen = async () => {
|
|
|
const taskId = route.query.taskId as string;
|
|
|
if (!taskId) {
|
|
|
return;
|
|
|
}
|
|
|
if (!currentNodeCode.value) {
|
|
|
await loadCurrentWorkflowTask();
|
|
|
}
|
|
|
if (currentNodeCode.value === 'leader_review') {
|
|
|
const valid = await validateMainForm();
|
|
|
if (!valid) {
|
|
|
return;
|
|
|
}
|
|
|
// 领导审批:后端原子完成审核落库+流转,前端不再调 completeTask
|
|
|
await leaderReviewAndCompleteTempTask({ ...form.value, taskId });
|
|
|
proxy?.$modal.msgSuccess('领导审批成功');
|
|
|
await proxy?.$tab.closePage(route);
|
|
|
router.go(-1);
|
|
|
return;
|
|
|
} else if (currentNodeCode.value === 'assignee_review') {
|
|
|
// 主执行人审阅:先回写可改字段,再重新装配变量后弹出审阅确认
|
|
|
await assigneeReviewTempTask(form.value);
|
|
|
buildVariables();
|
|
|
} else {
|
|
|
// 标准路径:装配变量后弹出提交流程确认
|
|
|
buildVariables();
|
|
|
}
|
|
|
await submitVerifyRef.value?.openDialog(taskId);
|
|
|
};
|
|
|
|
|
|
const handleApprovalRecord = () => {
|
|
|
if (form.value.tempTaskId) {
|
|
|
approvalRecordRef.value?.init(form.value.tempTaskId);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const submitCallback = async () => {
|
|
|
await proxy?.$tab.closePage(route);
|
|
|
router.go(-1);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 兜底修复:流程已到 leader_final 但 taskStatus 仍为 3 时,
|
|
|
* 调用后端补齐 totalHours 汇总 + lockFlag 锁定 + taskStatus→4。
|
|
|
*/
|
|
|
const syncTaskStateToPendingFinal = async (tempTaskId: string | number) => {
|
|
|
try {
|
|
|
await syncTaskState(tempTaskId);
|
|
|
} catch {
|
|
|
// 静默失败——canScoreClose/scoreAndClose 已有兜底逻辑
|
|
|
}
|
|
|
};
|
|
|
|
|
|
onMounted(async () => {
|
|
|
pageLoading.value = true;
|
|
|
try {
|
|
|
pageType.value = (route.query.type as string) || 'add';
|
|
|
await getDeptList();
|
|
|
await loadCurrentWorkflowTask();
|
|
|
const id = route.query.id as string | number | undefined;
|
|
|
if (id) {
|
|
|
await loadDetail(id);
|
|
|
// Why:执行人可能绕过 submitFinish 通过工作流通用审批将流程推到 leader_final,
|
|
|
// 此时 taskStatus 仍为 3(执行中) 而非 4(待领导审核)。前端加载时主动修复此不一致。
|
|
|
if (form.value.taskStatus === '3' && currentNodeCode.value === 'leader_final') {
|
|
|
await syncTaskStateToPendingFinal(id);
|
|
|
form.value.taskStatus = '4';
|
|
|
}
|
|
|
}
|
|
|
} finally {
|
|
|
pageLoading.value = false;
|
|
|
}
|
|
|
});
|
|
|
</script>
|