feat(oa/erp): 新增报表页面并优化任务管理功能

1.  重构临时任务API接口,拆分提交与列表查询方法,重命名方法名提升可读性
2.  完善临时任务相关类型定义,新增字段适配业务需求
3.  新增临时任务统计报表页面,支持多维度数据分析与导出
4.  优化主列表页面:调整搜索栏布局、新增业务状态筛选、重构表格列配置、增加编辑权限校验
5.  新增项目选择弹窗组件集成,优化表单重置逻辑
dev
zch 3 days ago
parent 843de4dc64
commit 57b270df36

@ -40,7 +40,15 @@ export const delTempTask = (tempTaskId: string | number | Array<string | number>
});
};
export const submitTempTask = (data: TempTaskForm): AxiosPromise<TempTaskVO> => {
export const getTempTaskList = (query?: TempTaskQuery): AxiosPromise<TempTaskVO[]> => {
return request({
url: '/oa/erp/tempTask/getErpTempTaskList',
method: 'get',
params: query
});
};
export const tempTaskSubmitAndFlowStart = (data: TempTaskForm): AxiosPromise<TempTaskVO> => {
return request({
url: '/oa/erp/tempTask/submit',
method: 'post',
@ -56,14 +64,14 @@ export const closeTempTask = (data: TempTaskForm) => {
});
};
export const listTempTaskChanges = (tempTaskId: string | number): AxiosPromise<TempTaskChangeVO[]> => {
export const listTempTaskChange = (tempTaskId: string | number): AxiosPromise<TempTaskChangeVO[]> => {
return request({
url: `/oa/erp/tempTask/${tempTaskId}/changes`,
method: 'get'
});
};
export const saveTempTaskChange = (data: TempTaskChangeForm) => {
export const addTempTaskChange = (data: TempTaskChangeForm) => {
return request({
url: '/oa/erp/tempTask/change',
method: 'post',
@ -78,11 +86,3 @@ export const approveTempTaskChange = (data: TempTaskChangeForm) => {
data
});
};
export const getErpTempTaskList = (query?: TempTaskQuery): AxiosPromise<TempTaskVO[]> => {
return request({
url: '/oa/erp/tempTask/getErpTempTaskList',
method: 'get',
params: query
});
};

@ -1,11 +1,11 @@
export interface TempTaskVO {
tempTaskId: string | number;
tempTaskCode?: string;
tempTaskCode: string;
taskTitle?: string;
taskDesc?: string;
taskDesc: string;
priority?: string;
urgentReason?: string;
requireTime?: string;
requireTime: string;
confirmFinishTime?: string;
actualStartTime?: string;
actualFinishTime?: string;
@ -87,6 +87,8 @@ export interface TempTaskForm extends BaseEntity {
ccUserIds?: string;
ossId?: string;
remark?: string;
changeCount?: number;
pendingChangeCount?: number;
flowCode?: string;
variables?: Record<string, unknown>;
bizExt?: Record<string, unknown>;
@ -98,16 +100,21 @@ export interface TempTaskQuery extends PageQuery {
taskDesc?: string;
priority?: string;
projectId?: string | number;
projectCode?: string;
projectName?: string;
requesterId?: string | number;
requesterName?: string;
requestDeptId?: string | number;
requestDeptName?: string;
dispatcherId?: string | number;
dispatcherName?: string;
softwareLeaderId?: string | number;
softwareLeaderName?: string;
assigneeId?: string | number;
assigneeName?: string;
finishResult?: string;
terminateReason?: string;
relatedTaskId?: string | number;
relatedTaskCode?: string;
relateReason?: string;
taskStatus?: string;
flowStatus?: string;
params?: Record<string, unknown>;
@ -117,8 +124,8 @@ export interface TempTaskChangeVO {
changeId: string | number;
tempTaskId: string | number;
tempTaskCode?: string;
changeType?: string;
changeReason?: string;
changeType: string;
changeReason: string;
beforeContent?: string;
afterContent?: string;
workloadEffect?: string;
@ -146,6 +153,12 @@ export interface TempTaskChangeForm extends BaseEntity {
workloadEffect?: string;
timeEffect?: string;
scopeEffect?: string;
changeUserId?: string | number;
changeUserName?: string;
approveResult?: string;
approverId?: string | number;
approverName?: string;
approveComment?: string;
approveTime?: string;
flowStatus?: string;
}

File diff suppressed because it is too large Load Diff

@ -15,6 +15,13 @@
<el-option v-for="dict in temp_task_priority" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="项目" prop="projectName">
<el-input v-model="queryParams.projectName" placeholder="请选择项目" clearable readonly>
<template #suffix>
<el-icon class="cursor-pointer" @click="openProjectSelect"><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="发起人" prop="requesterName">
<el-input v-model="queryParams.requesterName" placeholder="请输入发起人" clearable @keyup.enter="handleQuery" />
</el-form-item>
@ -24,8 +31,10 @@
<el-form-item label="执行人" prop="assigneeName">
<el-input v-model="queryParams.assigneeName" placeholder="请输入执行人" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="项目" prop="projectName">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable @keyup.enter="handleQuery" />
<el-form-item label="业务状态" prop="taskStatus">
<el-select v-model="queryParams.taskStatus" placeholder="请选择业务状态" clearable>
<el-option v-for="dict in temp_task_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="完成结果" prop="finishResult">
<el-select v-model="queryParams.finishResult" placeholder="请选择完成结果" clearable>
@ -43,93 +52,109 @@
<el-card shadow="never">
<template #header>
<div class="toolbar-row">
<div>
<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="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['oa:erp:tempTask:edit']"
>修改</el-button
>
<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="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>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['oa:erp:tempTask:export']"></el-button>
</div>
</el-col>
<right-toolbar v-model:showSearch="showSearch" :columns="columns" :search="true" @queryTable="getList" />
</div>
</el-row>
</template>
<el-tabs v-model="activeTab" class="mb-2" @tab-change="handleTabChange">
<el-tab-pane label="全部" name="all" />
<el-tab-pane label="草稿" name="draft" />
<el-tab-pane label="在办/未关闭" name="running" />
<el-tab-pane label="已结束" name="finished" />
<el-tab-pane label="全部" name="all" />
</el-tabs>
<el-table v-loading="loading" border :data="tempTaskList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="45" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="任务编号" prop="tempTaskCode" min-width="150" show-overflow-tooltip v-if="columns[0].visible" />
<el-table-column label="任务描述" prop="taskDesc" min-width="220" show-overflow-tooltip v-if="columns[1].visible" />
<el-table-column label="优先级" prop="priority" width="90" align="center" v-if="columns[2].visible">
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" v-if="columns[0].visible" />
<el-table-column label="任务编号" prop="tempTaskCode" min-width="150" align="center" v-if="columns[1].visible" />
<el-table-column label="任务标题" prop="taskTitle" min-width="160" show-overflow-tooltip align="center" v-if="columns[2].visible" />
<el-table-column label="任务描述" prop="taskDesc" min-width="220" show-overflow-tooltip align="center" v-if="columns[3].visible" />
<el-table-column label="优先级" prop="priority" width="90" align="center" v-if="columns[4].visible">
<template #default="scope">
<dict-tag :options="temp_task_priority" :value="scope.row.priority" />
</template>
</el-table-column>
<el-table-column label="发起人" prop="requesterName" width="110" align="center" v-if="columns[3].visible" />
<el-table-column label="发起部门" prop="requestDeptName" min-width="130" show-overflow-tooltip v-if="columns[4].visible" />
<el-table-column label="指派人" prop="dispatcherName" width="110" align="center" v-if="columns[5].visible" />
<el-table-column label="执行人" prop="assigneeName" width="110" align="center" v-if="columns[6].visible" />
<el-table-column label="项目" prop="projectName" min-width="160" show-overflow-tooltip v-if="columns[7].visible" />
<el-table-column label="需求时间" prop="requireTime" width="160" align="center" v-if="columns[8].visible">
<el-table-column label="需求时间" prop="requireTime" width="150" align="center" v-if="columns[5].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.requireTime, '{y}-{m}-{d} {h}:{i}') }}</span>
<span>{{ parseTime(scope.row.requireTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="确认完成" prop="confirmFinishTime" width="160" align="center" v-if="columns[9].visible">
<el-table-column label="确认完成时间" prop="confirmFinishTime" width="150" align="center" v-if="columns[6].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.confirmFinishTime, '{y}-{m}-{d} {h}:{i}') }}</span>
<span>{{ parseTime(scope.row.confirmFinishTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="预计工时" prop="estimateWorkload" width="100" align="right" v-if="columns[10].visible" />
<el-table-column label="实际工时" prop="actualWorkload" width="100" align="right" v-if="columns[11].visible" />
<el-table-column label="业务状态" prop="taskStatus" width="100" align="center" v-if="columns[12].visible">
<el-table-column label="项目编号" prop="projectCode" width="140" align="center" v-if="columns[7].visible" />
<el-table-column label="项目名称" prop="projectName" min-width="170" show-overflow-tooltip align="center" v-if="columns[8].visible" />
<el-table-column label="发起人" prop="requesterName" width="100" align="center" v-if="columns[9].visible" />
<el-table-column label="发起部门" prop="requestDeptName" width="130" align="center" v-if="columns[10].visible" />
<el-table-column label="指派人" prop="dispatcherName" width="100" align="center" v-if="columns[11].visible" />
<el-table-column label="软件部领导" prop="softwareLeaderName" width="120" align="center" v-if="columns[12].visible" />
<el-table-column label="主执行人" prop="assigneeName" width="110" align="center" v-if="columns[13].visible" />
<el-table-column label="预计工时" prop="estimateWorkload" width="100" align="center" v-if="columns[14].visible" />
<el-table-column label="实际工时" prop="actualWorkload" width="100" align="center" v-if="columns[15].visible" />
<el-table-column label="业务状态" prop="taskStatus" width="100" align="center" v-if="columns[16].visible">
<template #default="scope">
<dict-tag :options="temp_task_status" :value="scope.row.taskStatus" />
</template>
</el-table-column>
<el-table-column label="流程状态" prop="flowStatus" width="110" align="center" v-if="columns[13].visible">
<el-table-column label="流程状态" prop="flowStatus" width="100" align="center" v-if="columns[17].visible">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.flowStatus" />
</template>
</el-table-column>
<el-table-column label="完成结果" prop="finishResult" width="130" align="center" v-if="columns[14].visible">
<el-table-column label="完成结果" prop="finishResult" width="130" align="center" v-if="columns[18].visible">
<template #default="scope">
<dict-tag :options="temp_task_finish_result" :value="scope.row.finishResult" />
</template>
</el-table-column>
<el-table-column label="变更" width="90" align="center" v-if="columns[15].visible">
<el-table-column label="待审批变更" prop="pendingChangeCount" width="110" align="center" v-if="columns[19].visible" />
<el-table-column label="创建时间" prop="createTime" width="160" align="center" v-if="columns[20].visible">
<template #default="scope">
<el-tag :type="scope.row.pendingChangeCount > 0 ? 'warning' : 'info'">
{{ scope.row.pendingChangeCount || 0 }}/{{ scope.row.changeCount || 0 }}
</el-tag>
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="220" align="center" class-name="small-padding fixed-width">
<el-table-column label="操作" align="center" fixed="right" width="190" 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)" />
</el-tooltip>
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['oa:erp:tempTask:edit']" />
<el-button
v-if="canEdit(scope.row)"
link
type="primary"
icon="Edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['oa:erp:tempTask:edit']"
/>
</el-tooltip>
<el-tooltip content="提交" placement="top">
<el-button link type="success" icon="Promotion" @click="handleSubmit(scope.row)" v-hasPermi="['oa:erp:tempTask:submit']" />
</el-tooltip>
<el-tooltip content="变更" placement="top">
<el-button link type="warning" icon="RefreshRight" @click="handleChange(scope.row)" v-hasPermi="['oa:erp:tempTask:change']" />
</el-tooltip>
<el-tooltip content="关闭" placement="top">
<el-button link type="danger" icon="CircleClose" @click="handleClose(scope.row)" v-hasPermi="['oa:erp:tempTask:close']" />
<el-tooltip content="删除" placement="top">
<el-button
v-if="canEdit(scope.row)"
link
type="primary"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['oa:erp:tempTask:remove']"
/>
</el-tooltip>
</template>
</el-table-column>
@ -137,17 +162,30 @@
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<ProjectSelect ref="projectSelectRef" :multiple="false" @confirm-call-back="handleProjectSelect" />
</div>
</template>
<script setup name="TempTask" lang="ts">
import { delTempTask, listTempTask, submitTempTask } from '@/api/oa/erp/tempTask';
import { TempTaskQuery, TempTaskVO } from '@/api/oa/erp/tempTask/types';
import { FlowCodeEnum } from '@/enums/OAEnum';
import { delTempTask, listTempTask } from '@/api/oa/erp/tempTask';
import type { TempTaskQuery, TempTaskVO } from '@/api/oa/erp/tempTask/types';
import ProjectSelect from '@/components/ProjectSelect/index.vue';
import type { ProjectInfoVO } from '@/api/oa/erp/projectInfo/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { proxy } = getCurrentInstance() as any;
const router = useRouter();
const parseTime = (time?: string, pattern?: string) => {
if (!time) {
return '';
}
if (proxy?.parseTime) {
return proxy.parseTime(time, pattern);
}
return String(time).replace('T', ' ').slice(0, 16);
};
const { temp_task_priority, temp_task_status, temp_task_finish_result, wf_business_status } = toRefs<any>(
proxy?.useDict('temp_task_priority', 'temp_task_status', 'temp_task_finish_result', 'wf_business_status')
);
@ -159,26 +197,33 @@ const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const activeTab = ref('running');
const activeTab = ref('all');
const queryFormRef = ref<ElFormInstance>();
const projectSelectRef = ref<InstanceType<typeof ProjectSelect>>();
const columns = ref<FieldOption[]>([
{ key: 0, label: '任务编号', visible: true },
{ key: 1, label: '任务描述', visible: true },
{ key: 2, label: '优先级', visible: true },
{ key: 3, label: '发起人', visible: true },
{ key: 4, label: '发起部门', visible: true },
{ key: 5, label: '指派人', visible: true },
{ key: 6, label: '执行人', visible: true },
{ key: 7, label: '项目', visible: true },
{ key: 8, label: '需求时间', visible: true },
{ key: 9, label: '确认完成', visible: true },
{ key: 10, label: '预计工时', visible: true },
{ key: 11, label: '实际工时', visible: true },
{ key: 12, label: '业务状态', visible: true },
{ key: 13, label: '流程状态', visible: true },
{ key: 14, label: '完成结果', visible: true },
{ key: 15, label: '变更', visible: true }
{ key: 0, label: '序号', visible: true },
{ key: 1, label: '任务编号', visible: true },
{ key: 2, label: '任务标题', visible: true },
{ key: 3, label: '任务描述', visible: true },
{ key: 4, label: '优先级', visible: true },
{ key: 5, label: '需求时间', visible: true },
{ key: 6, label: '确认完成时间', visible: true },
{ key: 7, label: '项目编号', visible: true },
{ key: 8, label: '项目名称', visible: true },
{ key: 9, label: '发起人', visible: true },
{ key: 10, label: '发起部门', visible: true },
{ key: 11, label: '指派人', visible: true },
{ key: 12, label: '软件部领导', visible: true },
{ key: 13, label: '主执行人', visible: true },
{ key: 14, label: '预计工时', visible: true },
{ key: 15, label: '实际工时', visible: true },
{ key: 16, label: '业务状态', visible: true },
{ key: 17, label: '流程状态', visible: true },
{ key: 18, label: '完成结果', visible: true },
{ key: 19, label: '待审批变更', visible: true },
{ key: 20, label: '创建时间', visible: true }
]);
const queryParams = ref<TempTaskQuery>({
@ -187,38 +232,38 @@ const queryParams = ref<TempTaskQuery>({
tempTaskCode: undefined,
taskDesc: undefined,
priority: undefined,
projectId: undefined,
projectName: undefined,
requesterName: undefined,
dispatcherName: undefined,
assigneeName: undefined,
projectName: undefined,
taskStatus: undefined,
flowStatus: undefined,
finishResult: undefined,
taskStatus: '2',
params: {}
});
const applyTabQuery = () => {
queryParams.value.taskStatus = undefined;
queryParams.value.flowStatus = undefined;
queryParams.value.finishResult = undefined;
const applyTabFilter = () => {
if (activeTab.value === 'draft') {
queryParams.value.taskStatus = '1';
queryParams.value.finishResult = undefined;
} else if (activeTab.value === 'running') {
queryParams.value.taskStatus = '2';
queryParams.value.finishResult = undefined;
} else if (activeTab.value === 'finished') {
queryParams.value.taskStatus = '3';
} else {
queryParams.value.taskStatus = undefined;
queryParams.value.finishResult = undefined;
}
};
const getList = async () => {
loading.value = true;
try {
applyTabQuery();
const res = await listTempTask(queryParams.value);
tempTaskList.value = res.rows || [];
total.value = res.total || 0;
} finally {
loading.value = false;
}
applyTabFilter();
const res = await listTempTask(queryParams.value).finally(() => (loading.value = false));
tempTaskList.value = res.rows;
total.value = res.total;
};
const handleQuery = () => {
@ -228,6 +273,8 @@ const handleQuery = () => {
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.value.projectId = undefined;
queryParams.value.projectName = undefined;
handleQuery();
};
@ -241,12 +288,30 @@ const handleSelectionChange = (selection: TempTaskVO[]) => {
multiple.value = !selection.length;
};
const openProjectSelect = () => {
projectSelectRef.value?.open();
};
const handleProjectSelect = (data: ProjectInfoVO[]) => {
const project = data?.[0];
if (!project) {
return;
}
queryParams.value.projectId = project.projectId;
queryParams.value.projectCode = project.projectCode;
queryParams.value.projectName = project.projectName;
};
const canEdit = (row: TempTaskVO) => {
return !row.finishResult && ['draft', 'back', 'cancel'].includes(row.flowStatus || '');
};
const openEditPage = (type: string, id?: string | number) => {
router.push({
path: '/oa/erp/tempTask/edit',
query: {
id,
type
type,
...(id ? { id } : {})
}
});
};
@ -260,25 +325,10 @@ const handleView = (row: TempTaskVO) => {
};
const handleUpdate = (row?: TempTaskVO) => {
openEditPage('update', row?.tempTaskId || ids.value[0]);
};
const handleChange = (row: TempTaskVO) => {
openEditPage('change', row.tempTaskId);
};
const handleClose = (row: TempTaskVO) => {
openEditPage('close', row.tempTaskId);
};
const handleSubmit = async (row: TempTaskVO) => {
await proxy?.$modal.confirm(`是否确认提交临时任务"${row.tempTaskCode || row.taskTitle}"审批?`);
await submitTempTask({
...row,
flowCode: FlowCodeEnum.TEMP_TASK_CODE
});
proxy?.$modal.msgSuccess('提交成功');
await getList();
const id = row?.tempTaskId || ids.value[0];
if (id) {
openEditPage('update', id);
}
};
const handleDelete = async (row?: TempTaskVO) => {
@ -290,19 +340,16 @@ const handleDelete = async (row?: TempTaskVO) => {
};
const handleExport = () => {
proxy?.download('oa/erp/tempTask/export', { ...queryParams.value }, `tempTask_${new Date().getTime()}.xlsx`);
proxy?.download(
'oa/erp/tempTask/export',
{
...queryParams.value
},
`tempTask_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
</script>
<style scoped>
.toolbar-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
</style>

@ -0,0 +1,286 @@
<template>
<div class="p-2">
<el-card shadow="never" class="mb-3">
<el-form :model="queryParams" :inline="true" label-width="90px">
<el-form-item label="关闭月份">
<el-date-picker
v-model="finishMonth"
type="month"
value-format="YYYY-MM"
placeholder="请选择月份"
clearable
@change="handleQuery"
/>
</el-form-item>
<el-form-item label="执行人">
<el-input v-model="queryParams.assigneeName" placeholder="请输入执行人" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="项目">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="发起部门">
<el-input v-model="queryParams.requestDeptName" placeholder="请输入发起部门" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="完成结果">
<el-select v-model="queryParams.finishResult" placeholder="请选择完成结果" clearable>
<el-option v-for="dict in temp_task_finish_result" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<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-form-item>
</el-form>
</el-card>
<div class="metric-grid mb-3">
<el-card v-for="item in metrics" :key="item.label" shadow="never">
<div class="metric-label">{{ item.label }}</div>
<div class="metric-value">{{ item.value }}</div>
</el-card>
</div>
<el-card shadow="never">
<el-tabs v-model="activeDimension">
<el-tab-pane label="按人员" name="assignee" />
<el-tab-pane label="按项目" name="project" />
<el-tab-pane label="按发起部门" name="dept" />
<el-tab-pane label="按完成结果" name="result" />
<el-tab-pane label="按关联链" name="chain" />
</el-tabs>
<el-table v-loading="loading" :data="activeRows" border>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column :label="dimensionLabel" prop="name" min-width="220" show-overflow-tooltip align="center" />
<el-table-column label="任务数" prop="count" width="100" align="center" />
<el-table-column label="实际工时" prop="workload" width="120" align="center" />
<el-table-column label="平均工时" prop="avgWorkload" width="120" align="center" />
<el-table-column label="已完成" prop="finishedCount" width="100" align="center" />
<el-table-column label="不执行/终止" prop="terminatedCount" width="120" align="center" />
<el-table-column label="部分完成后终止" prop="partialTerminatedCount" width="140" align="center" />
</el-table>
<el-divider />
<el-table :data="detailRows" border>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="任务编号" prop="tempTaskCode" width="150" align="center" />
<el-table-column label="任务描述" prop="taskDesc" min-width="240" show-overflow-tooltip align="center" />
<el-table-column label="主执行人" prop="assigneeName" width="110" align="center" />
<el-table-column label="项目名称" prop="projectName" min-width="170" show-overflow-tooltip align="center" />
<el-table-column label="发起部门" prop="requestDeptName" width="130" align="center" />
<el-table-column label="完成结果" prop="finishResult" width="130" align="center">
<template #default="scope">
<dict-tag :options="temp_task_finish_result" :value="scope.row.finishResult" />
</template>
</el-table-column>
<el-table-column label="实际工时" prop="actualWorkload" width="100" align="center" />
<el-table-column label="关闭时间" prop="actualFinishTime" width="160" align="center">
<template #default="scope">
<span>{{ formatDateTime(scope.row.actualFinishTime) }}</span>
</template>
</el-table-column>
<el-table-column label="关联原任务" prop="relatedTaskCode" width="150" align="center" />
</el-table>
</el-card>
</div>
</template>
<script setup name="TempTaskReport" lang="ts">
import { listTempTask } from '@/api/oa/erp/tempTask';
import type { TempTaskQuery, TempTaskVO } from '@/api/oa/erp/tempTask/types';
interface AggregateRow {
name: string;
count: number;
workload: number;
avgWorkload: number;
finishedCount: number;
terminatedCount: number;
partialTerminatedCount: number;
}
const { proxy } = getCurrentInstance() as any;
const { temp_task_finish_result } = toRefs<any>(proxy?.useDict('temp_task_finish_result'));
const loading = ref(false);
const activeDimension = ref('assignee');
const currentMonth = () => {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
};
const finishMonth = ref(currentMonth());
const detailRows = ref<TempTaskVO[]>([]);
const runningTotal = ref(0);
const queryParams = ref<TempTaskQuery>({
pageNum: 1,
pageSize: 1000,
taskStatus: '3',
finishResult: undefined,
assigneeName: undefined,
projectName: undefined,
requestDeptName: undefined,
params: {}
});
const monthRange = computed(() => {
if (!finishMonth.value) {
return {};
}
const [year, month] = finishMonth.value.split('-').map(Number);
const end = new Date(year, month, 0);
return {
beginActualFinishTime: `${finishMonth.value}-01 00:00:00`,
endActualFinishTime: `${finishMonth.value}-${String(end.getDate()).padStart(2, '0')} 23:59:59`
};
});
const filteredRows = computed(() => {
return detailRows.value.filter((row) => row.finishResult && row.actualFinishTime);
});
const totalWorkload = computed(() => sumWorkload(filteredRows.value));
const metrics = computed(() => [
{ label: '闭环任务数', value: filteredRows.value.length },
{ label: '实际工时合计', value: formatNumber(totalWorkload.value) },
{ label: '单任务平均工时', value: formatNumber(filteredRows.value.length ? totalWorkload.value / filteredRows.value.length : 0) },
{ label: '未关闭任务数', value: runningTotal.value }
]);
const dimensionLabel = computed(() => {
const labelMap: Record<string, string> = {
assignee: '主执行人',
project: '项目',
dept: '发起部门',
result: '完成结果',
chain: '关联链'
};
return labelMap[activeDimension.value] || '维度';
});
const activeRows = computed(() => {
const fieldMap: Record<string, (row: TempTaskVO) => string> = {
assignee: (row) => row.assigneeName || '未填写执行人',
project: (row) => row.projectName || row.projectCode || '未绑定项目',
dept: (row) => row.requestDeptName || '未填写部门',
result: (row) => getFinishResultLabel(row.finishResult),
chain: (row) => row.relatedTaskCode || row.tempTaskCode || '未形成关联链'
};
return aggregateBy(filteredRows.value, fieldMap[activeDimension.value]);
});
const getFinishResultLabel = (value?: string) => {
const dictList = unref(temp_task_finish_result) || [];
const target = dictList.find((item: any) => item.value === value);
return target?.label || value || '未填写完成结果';
};
const sumWorkload = (rows: TempTaskVO[]) => {
return rows.reduce((sum, row) => sum + Number(row.actualWorkload || 0), 0);
};
const formatNumber = (value: number) => {
return Number(value.toFixed(2));
};
const formatDateTime = (value?: string) => {
if (!value) {
return '';
}
return String(value).replace('T', ' ').slice(0, 16);
};
const aggregateBy = (rows: TempTaskVO[], keyGetter: (row: TempTaskVO) => string) => {
const result = new Map<string, AggregateRow>();
rows.forEach((row) => {
const key = keyGetter(row);
const current =
result.get(key) ||
({
name: key,
count: 0,
workload: 0,
avgWorkload: 0,
finishedCount: 0,
terminatedCount: 0,
partialTerminatedCount: 0
} as AggregateRow);
current.count += 1;
current.workload += Number(row.actualWorkload || 0);
if (row.finishResult === '1') {
current.finishedCount += 1;
} else if (row.finishResult === '2') {
current.terminatedCount += 1;
} else if (row.finishResult === '3') {
current.partialTerminatedCount += 1;
}
current.avgWorkload = current.count ? formatNumber(current.workload / current.count) : 0;
current.workload = formatNumber(current.workload);
result.set(key, current);
});
return Array.from(result.values()).sort((a, b) => b.workload - a.workload);
};
const buildQuery = () => {
queryParams.value.params = { ...monthRange.value };
return { ...queryParams.value };
};
const getList = async () => {
loading.value = true;
try {
const res: any = await listTempTask(buildQuery());
detailRows.value = res.rows || [];
const runningRes: any = await listTempTask({ pageNum: 1, pageSize: 1, taskStatus: '2' });
runningTotal.value = runningRes.total || 0;
} finally {
loading.value = false;
}
};
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
const resetQuery = () => {
finishMonth.value = currentMonth();
queryParams.value.assigneeName = undefined;
queryParams.value.projectName = undefined;
queryParams.value.requestDeptName = undefined;
queryParams.value.finishResult = undefined;
handleQuery();
};
const handleExport = () => {
proxy?.download('oa/erp/tempTask/export', buildQuery(), `tempTask_report_${new Date().getTime()}.xlsx`);
};
onMounted(() => {
getList();
});
</script>
<style scoped>
.metric-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.metric-label {
color: var(--el-text-color-secondary);
font-size: 13px;
}
.metric-value {
margin-top: 8px;
color: var(--el-text-color-primary);
font-size: 26px;
font-weight: 600;
line-height: 1;
}
</style>
Loading…
Cancel
Save