refactor(oa/erp/tempTask): 完善临时任务报表与管理功能

1. 修复权限校验路径格式错误,将冒号分隔改为斜杠分隔
2. 新增报表多维度聚合逻辑与详细注释说明
3. 新增执行按钮与权限控制,优化列表页操作栏布局
4. 重构API接口注释与类型定义,新增syncTaskState兜底接口
5. 新增表单只读控制逻辑,修复评分关闭权限校验
6. 新增任务状态同步逻辑,修复流程节点与任务状态不一致问题
7. 优化提交流程与权限校验逻辑,补充详细业务注释
dev^2
zch 7 days ago
parent ce2f895c12
commit e2b59ada4f

@ -39,9 +39,10 @@ export const getTempTask = (tempTaskId: string | number): AxiosPromise<TempTaskV
};
/**
* 稿
* `taskStatus = '1'` `flowStatus = 'draft'`
* LSRW+yyyyMMdd+4
* 稿
* `taskStatus = '1'` `flowStatus = 'draft'`
* LSRW+yyyyMMdd+4
* /
*/
export const addTempTask = (data: TempTaskForm) => {
return request({
@ -52,8 +53,10 @@ export const addTempTask = (data: TempTaskForm) => {
};
/**
*
* 稿退`canEdit = true`
*
* 稿(`draft`)退(`back`)(`cancel`)
*
*
*/
export const updateTempTask = (data: TempTaskForm) => {
return request({
@ -64,8 +67,9 @@ export const updateTempTask = (data: TempTaskForm) => {
};
/**
*
* 稿/退/
*
* 稿(`taskStatus=1``flowStatus` draft/back/cancel)
*
*/
export const delTempTask = (tempTaskId: string | number | Array<string | number>) => {
return request({
@ -87,9 +91,16 @@ export const getTempTaskList = (query?: TempTaskQuery): AxiosPromise<TempTaskVO[
};
/**
* Warm-Flow
* (`taskStatus = '2'`) / `flowStatus = 'waiting'`
* variables(ignore=true, has_assignee) bizExt
* Warm-Flow
*
* API"暂存"
* 1.
* 2. (`taskStatus = '2'`) / `flowStatus = 'waiting'`
* 3. workflow variablesignore=true, has_assignee bizExt
* 4. `remoteWorkflowService.startCompleteTask`
*
* buildVariables() variables bizExt
*
*/
export const tempTaskSubmitAndFlowStart = (data: TempTaskForm): AxiosPromise<TempTaskVO> => {
return request({
@ -100,8 +111,9 @@ export const tempTaskSubmitAndFlowStart = (data: TempTaskForm): AxiosPromise<Tem
};
/**
* V3AD-12
* /// submitVerifycompleteTask
* V3AD-12
* ///
* submitVerify workflow completeTask
*/
export const assigneeReviewTempTask = (data: TempTaskForm) => {
return request({
@ -112,7 +124,9 @@ export const assigneeReviewTempTask = (data: TempTaskForm) => {
};
/**
* reassigned AD-12
* reassigned AD-12
* V3 使
* leaderReviewAndCompleteTempTask
*/
export const leaderReviewTempTaskVariables = (data: TempTaskForm): AxiosPromise<Record<string, unknown>> => {
return request({
@ -123,7 +137,15 @@ export const leaderReviewTempTaskVariables = (data: TempTaskForm): AxiosPromise<
};
/**
* Warm-Flow
* Warm-Flow V3
*
*
* 1.
* 2.
* 3. actualStartTime
* 4. workflow completeTask execute assignee_review
*
* "先回写变量再 completeTask"
*/
export const leaderReviewAndCompleteTempTask = (data: TempTaskForm) => {
return request({
@ -144,7 +166,9 @@ export const listTempTaskMember = (tempTaskId: string | number): AxiosPromise<Te
};
/**
* AD-13
*
* (`taskStatus=3`)AD-13
*
*/
export const addTempTaskMember = (data: TempTaskMemberForm) => {
return request({
@ -155,7 +179,9 @@ export const addTempTaskMember = (data: TempTaskMemberForm) => {
};
/**
*
*
* (memberType=1)
* (memberType=2)
*/
export const delTempTaskMember = (memberId: string | number) => {
return request({
@ -175,8 +201,9 @@ export const listTempTaskWorklog = (tempTaskId: string | number): AxiosPromise<T
};
/**
* /
* workDate ///
* /
* (`taskStatus=3`)(`lockFlag=0`)
* workDate ///
*/
export const saveTempTaskWorklog = (data: TempTaskWorklogForm) => {
return request({
@ -187,7 +214,9 @@ export const saveTempTaskWorklog = (data: TempTaskWorklogForm) => {
};
/**
*
*
* (`lockFlag=0`)
*
*/
export const delTempTaskWorklog = (worklogId: string | number) => {
return request({
@ -197,7 +226,15 @@ export const delTempTaskWorklog = (worklogId: string | number) => {
};
/**
* AD-14/AD-15
* AD-14/AD-15
*
*
* 1. lockFlag 0 1
* 2. totalHours
* 3. taskStatus 3() 4()
* 4. workflow completeTask execute leader_final
*
*
*/
export const submitFinishTempTask = (data: TempTaskFinishSubmitForm) => {
return request({
@ -208,7 +245,16 @@ export const submitFinishTempTask = (data: TempTaskFinishSubmitForm) => {
};
/**
* AD-05/AD-16
* AD-05/AD-16
*
*
* 1. scoreList erp_temp_task_score
* 2. leaderFinalOpinion
* 3. actualFinishTime
* 4. taskStatus 4() 5()
* 5. workflow completeTask leader_final
*
* "评分后再次打开"
*/
export const scoreAndCloseTempTask = (data: TempTaskScoreSubmitForm) => {
return request({
@ -219,7 +265,11 @@ export const scoreAndCloseTempTask = (data: TempTaskScoreSubmitForm) => {
};
/**
* AD-16
*
* SQL
* -
* - /
* AD-16 "领导看全部,参与人仅看本人"
*/
export const listTempTaskScore = (tempTaskId: string | number): AxiosPromise<TempTaskScoreVO[]> => {
return request({
@ -227,3 +277,15 @@ export const listTempTaskScore = (tempTaskId: string | number): AxiosPromise<Tem
method: 'get'
});
};
/**
* "待领导审核"(taskStatus=4)
* Why submitFinish leader_final
* taskStatus 3 submitFinish +
*/
export const syncTaskState = (tempTaskId: string | number) => {
return request({
url: `/oa/erp/tempTask/syncState/${tempTaskId}`,
method: 'post'
});
};

@ -1,82 +1,106 @@
/**
* VO ( ErpTempTaskVo )
* VO ErpTempTaskVo
*/
export interface TempTaskVO {
/** 临时任务唯一主键ID */
/** 临时任务唯一主键ID,由后端自增生成 */
tempTaskId: string | number;
/** 临时任务唯一全局编号 */
/**
* LSRW+yyyyMMdd+4
*
*/
tempTaskCode: string;
/** 临时任务标题 */
/** 临时任务标题,简短概括任务内容 */
taskTitle?: string;
/** 临时任务描述 */
/** 临时任务描述,详细说明任务背景与要求 */
taskDesc: string;
/** 任务类型1部门 2市场 3项目 */
/**
* AD-01
* 1-2-3-
*/
taskType?: string;
/** 计划开始时间 */
/** 计划开始时间,预估任务启动日期 */
planStartTime?: string;
/** 计划完成时间 */
/** 计划完成时间,预估任务截止日期 */
planEndTime?: string;
/** 实际开始时间 */
/**
* leader_review
*
*/
actualStartTime?: string;
/** 实际结束/关闭时间 */
/** 实际结束/关闭时间,领导评分关闭后后端自动落库 */
actualFinishTime?: string;
/** 主报工项目ID */
/** 主报工项目ID,仅 taskType=3 时必填 */
projectId?: string | number;
/** 项目编号快照 */
/** 项目编号快照,选择项目后自动填充,防止项目信息后续变更影响历史数据 */
projectCode?: string;
/** 项目名称快照 */
/** 项目名称快照,选择项目后自动填充 */
projectName?: string;
/** 归集部门ID */
/** 归集部门ID,仅 taskType=1 时必填,用于部门维度工时统计 */
deptId?: string | number;
/** 归集部门名称快照 */
/** 归集部门名称快照,选择部门后自动填充 */
deptName?: string;
/** 发起人用户ID */
/** 发起人用户ID,后端根据当前登录用户自动回填 */
requesterId?: string | number;
/** 发起人姓名快照 */
requesterName?: string;
/** 实际需求人ID */
/**
* ID
* AD-10
* realRequesterId
*/
realRequesterId?: string | number;
/** 实际需求人姓名快照 */
realRequesterName?: string;
/** 实际需求部门ID */
/** 实际需求部门ID,选择实际需求人时自动从用户信息带出 */
realRequestDeptId?: string | number;
/** 实际需求部门名称快照 */
realRequestDeptName?: string;
/** 软件部领导用户ID */
/** 软件部领导用户ID,用于工作流节点领导审批办理人计算 */
softwareLeaderId?: string | number;
/** 软件部领导姓名快照 */
softwareLeaderName?: string;
/** 主执行人用户ID */
/** 主执行人用户ID,任务执行阶段的负责人 */
assigneeId?: string | number;
/** 主执行人姓名快照 */
assigneeName?: string;
/** 预计工时 */
/** 预计工时,审批时预估的任务工作量 */
estimateWorkload?: number;
/** 累计总工时 */
/** 累计总工时,由后端按工时明细汇总计算 */
totalHours?: number;
/** 业务状态1暂存 2审批中 3执行中 4待领导审核 5已关闭 6作废 */
/**
* 稿稿
* 1- 2- 3- 4- 5- 6-
*/
taskStatus?: string;
/** 工作流状态 */
/**
* 使 wf_business_status
* draft(稿)/waiting()/finish()/invalid()/back(退)/termination()/cancel()
*/
flowStatus?: string;
/** 抄送人员用户ID */
/** 抄送人员用户ID,逗号分隔,用于流程完成后通知相关人员 */
ccUserIds?: string;
/** 备注 */
remark?: string;
/** 参与人列表 */
/** 参与人列表(主执行人+协作人),详情查询时由后端填充,列表查询不包含 */
members?: TempTaskMemberVO[];
/** 工时明细列表 */
/** 工时明细列表,详情查询时由后端填充,列表查询不包含 */
worklogs?: TempTaskWorklogVO[];
/** 可见评分列表 */
/** 可见评分列表领导看全部参与人仅看本人AD-16 */
scores?: TempTaskScoreVO[];
/** 创建时间 */
createTime?: string;
}
/**
*
*
* // RemoteStartProcess
*/
export interface TempTaskForm extends BaseEntity {
tempTaskId?: string | number;
/**
* ID
* completeTask leaderReviewAndCompleteTempTask
*/
taskId?: string | number;
tempTaskCode?: string;
taskTitle?: string;
@ -101,8 +125,11 @@ export interface TempTaskForm extends BaseEntity {
softwareLeaderName?: string;
assigneeId?: string | number;
assigneeName?: string;
/** 主执行人审阅意见,仅 assignee_review 节点可填写 */
assigneeOpinion?: string;
/** 领导审批意见 */
leaderOpinion?: string;
/** 领导最终审核意见,在评分关闭时填写 */
leaderFinalOpinion?: string;
estimateWorkload?: number;
totalHours?: number;
@ -110,16 +137,26 @@ export interface TempTaskForm extends BaseEntity {
flowStatus?: string;
ccUserIds?: string;
remark?: string;
/** 流程定义编码,固定传入 OATT */
/**
* OATT Warm-Flow OA
*
*/
flowCode?: string;
/** 工作流办理过程中传递的变量Map */
/**
* Map//
*
*/
variables?: Record<string, unknown>;
/** 流程实例业务扩展载荷 */
/**
* /
* businessId(ID)/businessCode()/businessTitle()
*/
bizExt?: Record<string, unknown>;
}
/**
*
*
* PageQuery
*/
export interface TempTaskQuery extends PageQuery {
tempTaskCode?: string;
@ -142,30 +179,43 @@ export interface TempTaskQuery extends PageQuery {
softwareLeaderId?: string | number;
taskStatus?: string;
flowStatus?: string;
/** 额外扩展查询 Map 载荷,如 beginActualFinishTime/endActualFinishTime */
/**
* Map
* beginActualFinishTime/endActualFinishTime
*/
params?: Record<string, unknown>;
}
/**
* VO
* VO
* 1 + N
*/
export interface TempTaskMemberVO {
memberId: string | number;
tempTaskId: string | number;
/** 参与类型1主执行人 2协作人 */
/**
*
* 1-2-
*/
memberType: string;
userId: string | number;
userName?: string;
/** 参与人所属部门ID从用户信息带出 */
memberDeptId?: string | number;
/** 协作说明,仅协作人可填写,描述协作事项范围 */
joinRemark?: string;
}
/**
*
*
*
*/
export interface TempTaskMemberForm {
memberId?: string | number;
tempTaskId?: string | number;
/**
* '2'
*/
memberType?: string;
userId?: string | number;
userName?: string;
@ -182,43 +232,51 @@ export interface TempTaskWorklogVO {
memberId: string | number;
userId: string | number;
userName?: string;
/** 自然周周一 */
/** 自然周周一,由后端根据 workDate 自动推导 */
weekStart: string;
/** 自然周周日 */
/** 自然周周日,由后端根据 workDate 自动推导 */
weekEnd: string;
/** 工作日期 */
workDate: string;
/** 本条工时,最小0.5 */
/** 本条工时,最小 0.5 小时,步进 0.5 */
hours: number;
workContent: string;
ossId?: string;
/** 锁定标志0可编辑 1已锁定 */
/** 锁定标志0可编辑(执行中)1已锁定(提交完成后整单锁定,防止篡改历史工时) */
lockFlag: string;
}
/**
*
* /
* workDate ///
*/
export interface TempTaskWorklogForm {
worklogId?: string | number;
tempTaskId?: string | number;
/** 工作日期,后端据此推导自然周(周一到周日) */
workDate?: string;
/** 工时,最小值 0.5,步进 0.5 */
hours?: number;
workContent?: string;
ossId?: string;
}
/**
*
* AD-14/AD-15
* lockFlag 1
* (3) (4) leader_final
*/
export interface TempTaskFinishSubmitForm {
tempTaskId: string | number;
/** 工作流待办任务ID用于 completeTask 完成当前执行节点 */
taskId: string | number;
/** 主执行人完成意见 */
finishOpinion?: string;
}
/**
* VO
* VO
* +AD-16
*/
export interface TempTaskScoreVO {
scoreId: string | number;
@ -226,17 +284,21 @@ export interface TempTaskScoreVO {
memberId: string | number;
userId: string | number;
userName?: string;
/** 评分等级A++ A+ A B C */
/** 评分等级,字典 temp_task_scoreA++ / A+ / A / B / C */
scoreGrade: string;
scoreRemark?: string;
/** 评分人用户ID */
scorerId?: string | number;
/** 评分时间 */
scoreTime?: string;
}
/**
*
* scoreList
*
*/
export interface TempTaskScoreForm {
/** 参与人记录ID用于关联 member 表 */
memberId?: string | number;
userId: string | number;
scoreGrade: string;
@ -244,11 +306,16 @@ export interface TempTaskScoreForm {
}
/**
*
* AD-05/AD-16
* +
* + + +
*/
export interface TempTaskScoreSubmitForm {
tempTaskId: string | number;
/** 工作流待办任务ID用于 completeTask 完成 leader_final 节点 */
taskId: string | number;
/** 领导最终审核意见 */
leaderFinalOpinion?: string;
/** 参与人评分列表,必须覆盖全部参与人(主执行人+所有协作人) */
scoreList: TempTaskScoreForm[];
}

@ -11,10 +11,10 @@
:pageType="pageType"
:mode="false"
>
<el-button v-if="canSubmitFinish" type="success" icon="CircleCheck" @click="handleSubmitFinish" v-hasPermi="['oa:erp:tempTask:submitFinish']">
<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:score']">
<el-button v-if="canScoreClose" type="warning" icon="Star" @click="openScoreDialog" v-hasPermi="['oa/erp:tempTask:list']">
评分关闭
</el-button>
</approvalButton>
@ -136,6 +136,7 @@
</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="流程自动记录" />
@ -163,7 +164,7 @@
icon="Plus"
size="small"
@click="openMemberDialog"
v-hasPermi="['oa:erp:tempTask:worklog']"
v-hasPermi="['oa/erp:tempTask:list']"
>
协作人
</el-button>
@ -198,7 +199,7 @@
icon="Plus"
size="small"
@click="openWorklogDialog()"
v-hasPermi="['oa:erp:tempTask:worklog']"
v-hasPermi="['oa/erp:tempTask:list']"
>
填报工时
</el-button>
@ -247,7 +248,7 @@
icon="Star"
size="small"
@click="openScoreDialog"
v-hasPermi="['oa:erp:tempTask:score']"
v-hasPermi="['oa/erp:tempTask:list']"
>
评分关闭
</el-button>
@ -377,7 +378,8 @@ import {
scoreAndCloseTempTask,
submitFinishTempTask,
tempTaskSubmitAndFlowStart,
updateTempTask
updateTempTask,
syncTaskState
} from '@/api/oa/erp/tempTask';
import type {
TempTaskForm,
@ -522,11 +524,49 @@ const scoreForm = ref<TempTaskScoreSubmitForm>({
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 === 'approval' && !['leader_review', 'assignee_review'].includes(currentNodeCode.value))
() => 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 &&
@ -535,12 +575,25 @@ const canSubmitFinish = computed(
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' &&
form.value.taskStatus === '4' &&
// Why submitFinish leader_final
// taskStatus 3 leader_final
(form.value.taskStatus === '3' || form.value.taskStatus === '4') &&
currentNodeCode.value === 'leader_final'
);
@ -586,6 +639,12 @@ const handleRealDeptChange = (deptId?: string | number) => {
form.value.realRequestDeptName = findDept(deptId)?.deptName;
};
/**
* 监听 taskType 切换联动清除不适用的表单字段
* - 切换到非项目类型(taskType !== '3')清除项目相关字段避免脏数据带入提交
* - 切换到非部门类型(taskType !== '1')清除部门相关字段
* 此逻辑确保表单提交时不会携带与当前任务类型无关的字段值
*/
watch(
() => form.value.taskType,
(taskType) => {
@ -655,6 +714,14 @@ const handleUserSelect = (users: Array<Record<string, any>>) => {
}
};
/**
* 从专用子表端点加载参与人/工时/评分列表AD-13/AD-16
*
* 这些子表通过独立 API 端点查询而非依赖主表 queryById 的嵌套返回
* 为什么不用 loadDetail 中的嵌套数据而要再调一次
* 主表 queryById 可能出于性能考虑不返回完整子表数据不包含 members/worklogs/scores
* 或者列表页查询不携带子表只有编辑页才加载loadSubLists 保证子表数据是最新状态
*/
const loadSubLists = async () => {
if (!form.value.tempTaskId) {
memberList.value = [];
@ -673,6 +740,18 @@ const loadSubLists = async () => {
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 = {
@ -680,9 +759,11 @@ const loadDetail = async (id: string | number) => {
...res.data,
flowCode: FlowCodeEnum.TEMP_TASK_CODE
};
//
memberList.value = res.data?.members || [];
worklogList.value = res.data?.worklogs || [];
scoreList.value = res.data?.scores || [];
//
await loadSubLists();
};
@ -696,9 +777,24 @@ const loadCurrentWorkflowTask = async () => {
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,
@ -719,17 +815,20 @@ const buildVariables = () => {
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)
businessTitle: businessTitle.slice(0, 80) //
};
};
@ -836,12 +935,27 @@ const handleDeleteWorklog = async (worklogId: string | number) => {
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();
}
@ -864,6 +978,17 @@ const handleSubmitFinish = async () => {
}
};
/**
* 打开评分关闭对话框
*
* 预填充逻辑复用已有评分
* - 遍历所有参与人(memberList)为每个参与人创建一条评分表单
* - 如果后端已有评分数据例如领导之前打开过评分弹窗但未提交
* 则用已有评分预填充 scoreGrade scoreRemark
* 方便领导再次打开时不丢失之前填写的评分内容
* - 为什么需要这个预填充评分关闭是一次性操作但领导可能多次打开弹窗查看/调整
* 已有的评分数据不应在重新打开时丢失
*/
const openScoreDialog = () => {
const taskId = route.query.taskId as string | undefined;
if (!form.value.tempTaskId || !taskId) {
@ -878,6 +1003,7 @@ const openScoreDialog = () => {
return {
memberId: member.memberId,
userId: member.userId,
//
scoreGrade: oldScore?.scoreGrade || '',
scoreRemark: oldScore?.scoreRemark
};
@ -886,7 +1012,17 @@ const openScoreDialog = () => {
scoreDialog.visible = true;
};
/**
* 领导评分关闭提交
*
* 校验所有参与人的评分等级必须填写不能有遗漏
* 提交后行为
* - 后端逐人写评分 + 落库 actualFinishTime + 关闭任务(taskStatus=5)
* - 完成 leader_final 工作流节点
* - 关闭页面返回列表
*/
const handleScoreAndClose = async () => {
//
if (scoreForm.value.scoreList.some((item) => !item.scoreGrade)) {
proxy?.$modal.msgWarning('请为全部参与人选择评分等级');
return;
@ -903,6 +1039,28 @@ const handleScoreAndClose = async () => {
}
};
/**
* 审批按钮回调根据当前工作流节点做三路分发
*
* 分支逻辑
* 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) {
@ -916,15 +1074,18 @@ const approvalVerifyOpen = async () => {
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);
@ -941,6 +1102,18 @@ const submitCallback = async () => {
router.go(-1);
};
/**
* 兜底修复流程已到 leader_final taskStatus 仍为 3
* 调用后端补齐 totalHours 汇总 + lockFlag 锁定 + taskStatus4
*/
const syncTaskStateToPendingFinal = async (tempTaskId: string | number) => {
try {
await syncTaskState(tempTaskId);
} catch {
// canScoreClose/scoreAndClose
}
};
onMounted(async () => {
pageLoading.value = true;
try {
@ -950,6 +1123,12 @@ onMounted(async () => {
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;

@ -54,20 +54,20 @@
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['oa:erp:tempTask:add']"></el-button>
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['oa/erp:tempTask:add']"></el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['oa:erp:tempTask:edit']">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['oa/erp:tempTask:edit']">
修改
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['oa:erp:tempTask:remove']">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['oa/erp:tempTask:remove']">
删除
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['oa:erp:tempTask:export']"></el-button>
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['oa/erp:tempTask:export']"></el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" :columns="columns" :search="true" @queryTable="getList" />
</el-row>
@ -135,7 +135,7 @@
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="190" class-name="small-padding fixed-width">
<el-table-column label="操作" align="center" fixed="right" width="240" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" />
@ -147,7 +147,17 @@
type="primary"
icon="Edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['oa:erp:tempTask:edit']"
v-hasPermi="['oa/erp:tempTask:edit']"
/>
</el-tooltip>
<el-tooltip content="执行" placement="top">
<el-button
v-if="canExecute(scope.row)"
link
type="success"
icon="Clock"
@click="handleExecute(scope.row)"
v-hasPermi="['oa/erp:tempTask:list']"
/>
</el-tooltip>
<el-tooltip content="删除" placement="top">
@ -157,7 +167,7 @@
type="primary"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['oa:erp:tempTask:remove']"
v-hasPermi="['oa/erp:tempTask:remove']"
/>
</el-tooltip>
</template>
@ -180,6 +190,7 @@ import ProjectSelect from '@/components/ProjectSelect/index.vue';
import type { ProjectInfoVO } from '@/api/oa/erp/projectInfo/types';
const { proxy } = getCurrentInstance() as any;
const route = useRoute();
const router = useRouter();
const parseTime = (time?: string, pattern?: string) => {
@ -249,6 +260,21 @@ const queryParams = ref<TempTaskQuery>({
params: {}
});
/**
* Tab 标签页与业务状态(taskStatus)的映射关系
*
* Tab 本质是对 taskStatus 的快捷筛选器
* - all: 不限制 taskStatus显示全部任务
* - draft: taskStatus=1(暂存)发起人可继续编辑或删除
* - approving: taskStatus=2(审批中)正在工作流审批链路中
* - running: taskStatus=3(执行中)主执行人和协作人可填报工时
* - pendingFinal: taskStatus=4(待领导审核)等待软件部领导评分关闭
* - closed: taskStatus=5(已关闭)归档只读
* - invalid: taskStatus=6(作废)归档只读
*
* tabStatusMap 的值直接赋值给 queryParams.taskStatus 作为后端过滤条件
* all 的值为 undefined表示不传 taskStatus 过滤参数
*/
const tabStatusMap: Record<string, string | undefined> = {
all: undefined,
draft: '1',
@ -259,6 +285,10 @@ const tabStatusMap: Record<string, string | undefined> = {
invalid: '6'
};
/**
* 根据当前激活的 Tab 设置 taskStatus 过滤条件
* getList 调用前执行确保查询参数携带正确的 taskStatus
*/
const applyTabFilter = () => {
queryParams.value.taskStatus = tabStatusMap[activeTab.value];
};
@ -319,17 +349,57 @@ const handleProjectSelect = (data: ProjectInfoVO[]) => {
queryParams.value.projectName = project.projectName;
};
/**
* 判断列表行是否可编辑显示修改按钮
*
* 可编辑条件flowStatus draft(草稿)/back(退回)/cancel(撤销) 时允许修改
* 为什么这三种状态可编辑
* - draft: 从未提交过可以任意修改
* - back: 被审批人退回发起人需修改后重新提交
* - cancel: 发起人主动撤销可修改后重新提交
*
* 不可编辑的状态
* - waiting(审批中): 修改会破坏当前审批数据一致性
* - finish(已完成): 已归档数据不允许修改
* - invalid(作废): 作废数据不允许修改
*/
const canEdit = (row: TempTaskVO) => {
return ['draft', 'back', 'cancel'].includes(row.flowStatus || '');
};
/**
* 判断列表行是否可删除显示删除按钮
*
* 可删除条件taskStatus === '1'(暂存) flowStatus draft/back/cancel
* 为什么需要同时检查 taskStatus flowStatus
* taskStatus 单独判断不够因为存在 taskStatus=1 flowStatus=waiting 的中间态
* 例如刚提交但流程尚未完全落库此时不应允许删除
* flowStatus 单独判断也不够因为存在 flowStatus=draft taskStatus 已变更的脏数据
* 双重检查确保只有当任务确实处于可删除阶段时才开放删除操作
*/
const canDelete = (row: TempTaskVO) => {
return row.taskStatus === '1' && ['draft', 'back', 'cancel'].includes(row.flowStatus || '');
};
/**
* 判断是否可进入执行页面显示"执行"按钮
* taskStatus=3(执行中) 时显示角色校验由后端负责
*/
const canExecute = (row: TempTaskVO) => {
return row.taskStatus === '3';
};
/**
* 打开执行页面专用于维护参与人和工时明细
*/
const handleExecute = (row: TempTaskVO) => {
openEditPage('execute', row.tempTaskId);
};
const openEditPage = (type: string, id?: string | number) => {
proxy.$tab.closePage(route);
router.push({
path: '/oa/erp/tempTask/edit',
path: '/timesheet/tempTask/edit',
query: {
type,
...(id ? { id } : {})

@ -24,7 +24,7 @@
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['oa:erp:tempTask:export']"></el-button>
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['oa/erp:tempTask:export']"></el-button>
</el-form-item>
</el-form>
</el-card>
@ -140,11 +140,23 @@ const queryParams = ref<TempTaskQuery>({
params: {}
});
/**
* 根据所选关闭月份推导 monthRange月初到月末的精确时间戳
*
* 为什么用 monthRange 而不直接用 finishMonth
* actualFinishTime datetime 字段数据库查询需要精确的起止时间戳
* `new Date(year, month, 0)` 获取的是该月最后一天的 Date 对象
* 通过 getDate() 得到当月天数自动处理 28/29/30/31 天的差异
*
* monthRange 通过 queryParams.params 传递到后端 MyBatis XML SQL 条件中
* 用于 `actualFinishTime BETWEEN beginActualFinishTime AND endActualFinishTime`
*/
const monthRange = computed(() => {
if (!finishMonth.value) {
return {};
}
const [year, month] = finishMonth.value.split('-').map(Number);
// new Date(year, month, 0) -> getDate() 2
const end = new Date(year, month, 0);
return {
beginActualFinishTime: `${finishMonth.value}-01 00:00:00`,
@ -152,6 +164,13 @@ const monthRange = computed(() => {
};
});
/**
* 前筛过滤掉无 actualFinishTime 的任务 + 按部门筛选
* 为什么 deptId 过滤放在前端而非 SQL
* 报表需要展示按人/项目/部门/评分四种聚合维度
* 如果部门过滤放在后端切换维度时前端无法按其他维度重新聚合已过滤数据
* 前端一次性拉取全量数据后在内存做多维度聚合部门过滤只是视图层的一个切片
*/
const filteredTasks = computed(() => {
return detailRows.value.filter((row) => {
if (!row.actualFinishTime) {
@ -164,6 +183,11 @@ const filteredTasks = computed(() => {
});
});
/**
* 将任务列表展平为工时明细行每行携带所属任务的快照字段projectName/deptName
* flatMap [task, task, ...] 展开为 [worklog, worklog, ...]
* 用于后续按人/项目/部门维度聚合
*/
const worklogRows = computed<ReportWorklogRow[]>(() => {
return filteredTasks.value.flatMap((task) =>
(task.worklogs || []).map((worklog) => ({
@ -176,6 +200,9 @@ const worklogRows = computed<ReportWorklogRow[]>(() => {
);
});
/**
* 将任务列表展平为评分记录行用于评分维度的聚合和分布统计
*/
const scoreRows = computed<ReportScoreRow[]>(() => {
return filteredTasks.value.flatMap((task) =>
(task.scores || []).map((score) => ({
@ -204,6 +231,19 @@ const dimensionLabel = computed(() => {
return labelMap[activeDimension.value] || '维度';
});
/**
* 根据当前激活的聚合维度计算上表格的数据行
*
* 四种维度
* - member(按人员工时): 以填报人姓名为 key 聚合工时和次数
* - project(按项目): 以关联项目名称为 key 聚合工时和次数
* - dept(按归集部门): 以归集部门名称为 key 聚合工时和次数
* - score(按评分): 以评分等级为 key 聚合评分次数不涉及工时
*
* 为什么前三种走 aggregateWorklogsscore aggregateScores
* 评分维度的聚合对象是评分记录而非工时记录两者的聚合逻辑和数据源完全不同
* 分开处理避免在 aggregateWorklogs 中塞入评分聚合的 if-else 分支
*/
const activeRows = computed(() => {
if (activeDimension.value === 'score') {
return aggregateScores();
@ -252,6 +292,23 @@ const formatDateTime = (value?: string) => {
return String(value).replace('T', ' ').slice(0, 16);
};
/**
* 按指定维度聚合工时明细
*
* keyGetter 决定分组键
* - member: 按填报人姓名分组
* - project: 按关联项目名称分组
* - dept: 按归集部门名称分组
*
* 聚合字段
* - taskCount: 通过 Set 去重 taskId 计数多个 worklog 属于同一任务也算 1 个任务
* - hours: 累加所有工时
* - scoreGrades: 通过 appendScores 追加评分数据用于评分分布统计
*
* 为什么 taskCount Set 去重
* 一个任务下可能有多个 worklog如果直接 count 行数会多算任务数
* 使用 Set 保证每个任务只计一次
*/
const aggregateWorklogs = (keyGetter: (row: ReportWorklogRow) => string) => {
const result = new Map<string, AggregateDraft>();
worklogRows.value.forEach((row) => {
@ -328,6 +385,10 @@ const memberNameById = (memberId: string | number) => {
return '';
};
/**
* Map<分组键, AggregateDraft> 转换为用于渲染的 AggregateRow[]
* 排序规则工时降序工时相同时按评分记录数降序
*/
const toAggregateRows = (result: Map<string, AggregateDraft>): AggregateRow[] => {
return Array.from(result.values())
.map((item) => {
@ -344,6 +405,20 @@ const toAggregateRows = (result: Map<string, AggregateDraft>): AggregateRow[] =>
.sort((a, b) => b.hours - a.hours || b.scoreCount - a.scoreCount);
};
/**
* 构建报表查询参数
*
* 关键参数说明
* - monthRange: 通过 params 传递关闭月份范围后端 SQL WHERE 条件中使用
* - deptId: 设为 undefined因为 deptId 的过滤在 filteredTasks 中前端处理
* 不在后端 SQL 中做避免部门维度与其他维度的交叉过滤产生歧义
* - includeDetail: true要求后端返回 members/worklogs/scores 子表数据
* 报表需要这些明细数据做前端聚合运算按人/项目/部门/评分多维度统计
*
* 为什么 includeDetail 设为 true
* 列表页查询不需要子表数据性能优化但报表页的前端聚合统计需要完整的工时明细和评分数据
* 所以通过 includeDetail 参数要求后端返回子表嵌套数据
*/
const buildQuery = () => {
queryParams.value.params = { ...monthRange.value };
return {
@ -361,11 +436,21 @@ const getDeptList = async () => {
deptList.value = res.data || [];
};
/**
* 获取报表数据列表
*
* 主查询拉取已关闭(taskStatus=5)的任务同时附带 includeDetail=true 获取子表明细
* 并行查询未关闭任务数(runningTotal)用于面板展示
* - taskStatus=2: 审批中
* - taskStatus=3: 执行中
* - taskStatus=4: 待领导审核
*/
const getList = async () => {
loading.value = true;
try {
const res: any = await listTempTask(buildQuery());
detailRows.value = res.rows || [];
// ""
const runningResList = await Promise.all(
['2', '3', '4'].map((status) => listTempTask({ pageNum: 1, pageSize: 1, taskStatus: status } as TempTaskQuery))
);

Loading…
Cancel
Save