feat(oa/erp): 新增报表页面并优化任务管理功能
1. 重构临时任务API接口,拆分提交与列表查询方法,重命名方法名提升可读性 2. 完善临时任务相关类型定义,新增字段适配业务需求 3. 新增临时任务统计报表页面,支持多维度数据分析与导出 4. 优化主列表页面:调整搜索栏布局、新增业务状态筛选、重构表格列配置、增加编辑权限校验 5. 新增项目选择弹窗组件集成,优化表单重置逻辑dev
parent
843de4dc64
commit
57b270df36
File diff suppressed because it is too large
Load Diff
@ -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…
Reference in New Issue