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.

1138 lines
44 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" 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 在审批模式也可编辑:
* 这两个节点的业务语义是"审批者可以修正业务数据",而非纯粹的"只读审批"。
* 审批即修改,所以表单在这些节点保持可编辑状态。
*/
// Whytype=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>