数据分析页面-多维度分析增加任务明细类型分析(一级、二级、三级分类)

This commit is contained in:
Your Name 2026-01-23 10:49:06 +08:00
parent 3fcc41855a
commit 647e9a777d
4 changed files with 519 additions and 2 deletions

View File

@ -1975,6 +1975,12 @@ public class AnalyticsService {
result.setDemandDeptAnalysis(buildDemandDeptAnalysis(tasks));
result.setPlatformAnalysis(buildPlatformAnalysis(tasks));
// 构建任务类型分析(一级、二级、三级)
Map<Integer, List<DimensionAnalysisVO.TaskTypeAnalysis>> taskTypeAnalysisMap = buildTaskTypeAnalysis(tasks);
result.setTaskTypeAnalysis(taskTypeAnalysisMap.getOrDefault(1, new ArrayList<>()));
result.setTaskTypeLevel2Analysis(taskTypeAnalysisMap.getOrDefault(2, new ArrayList<>()));
result.setTaskTypeLevel3Analysis(taskTypeAnalysisMap.getOrDefault(3, new ArrayList<>()));
return result;
}
@ -2857,5 +2863,258 @@ public class AnalyticsService {
return result;
}
/**
* 构建任务类型分析数据(一级、二级、三级分类)
* @param tasks 任务列表
* @return 按层级分组的任务类型分析数据,key为层级(1/2/3)
*/
private Map<Integer, List<DimensionAnalysisVO.TaskTypeAnalysis>> buildTaskTypeAnalysis(List<Task> tasks) {
log.info("开始构建任务类型分析数据,任务数量: {}", tasks.size());
// 获取所有任务类型
List<DemandType> allTypes = demandTypeMapper.selectList(
new LambdaQueryWrapper<DemandType>()
.eq(DemandType::getDeleted, 0)
);
// 构建类型ID到类型的映射
Map<Long, DemandType> typeMap = allTypes.stream()
.collect(Collectors.toMap(DemandType::getId, t -> t, (a, b) -> a));
// 构建父类型名称映射
Map<Long, String> typeNameMap = allTypes.stream()
.collect(Collectors.toMap(DemandType::getId, t -> t.getName() != null ? t.getName() : "未知", (a, b) -> a));
// 收集所有任务中涉及的任务类型ID
Set<Long> allTaskTypeIds = new HashSet<>();
for (Task task : tasks) {
// 单个任务类型ID
if (task.getTaskTypeId() != null) {
allTaskTypeIds.add(task.getTaskTypeId());
}
// 多个任务类型ID(逗号分隔)
if (task.getTaskTypeIds() != null && !task.getTaskTypeIds().trim().isEmpty()) {
String[] ids = task.getTaskTypeIds().split(",");
for (String id : ids) {
try {
allTaskTypeIds.add(Long.parseLong(id.trim()));
} catch (NumberFormatException e) {
// 忽略无效ID
}
}
}
}
log.info("涉及的任务类型ID数量: {}", allTaskTypeIds.size());
// 按任务类型ID分组统计任务
Map<Long, List<Task>> tasksByTypeId = new HashMap<>();
for (Task task : tasks) {
Set<Long> typeIds = new HashSet<>();
if (task.getTaskTypeId() != null) {
typeIds.add(task.getTaskTypeId());
}
if (task.getTaskTypeIds() != null && !task.getTaskTypeIds().trim().isEmpty()) {
String[] ids = task.getTaskTypeIds().split(",");
for (String id : ids) {
try {
typeIds.add(Long.parseLong(id.trim()));
} catch (NumberFormatException e) {
// 忽略无效ID
}
}
}
for (Long typeId : typeIds) {
tasksByTypeId.computeIfAbsent(typeId, k -> new ArrayList<>()).add(task);
}
}
// 计算每个类型的层级
Map<Long, Integer> typeLevelMap = new HashMap<>();
for (DemandType type : allTypes) {
int level = calculateTypeLevel(type.getId(), typeMap);
typeLevelMap.put(type.getId(), level);
}
// 构建各层级的分析数据
Map<Integer, List<DimensionAnalysisVO.TaskTypeAnalysis>> result = new HashMap<>();
result.put(1, new ArrayList<>());
result.put(2, new ArrayList<>());
result.put(3, new ArrayList<>());
for (Long typeId : allTaskTypeIds) {
DemandType type = typeMap.get(typeId);
if (type == null) {
continue;
}
List<Task> typeTasks = tasksByTypeId.getOrDefault(typeId, new ArrayList<>());
if (typeTasks.isEmpty()) {
continue;
}
int level = typeLevelMap.getOrDefault(typeId, 1);
// 构建分析数据
DimensionAnalysisVO.TaskTypeAnalysis analysis = buildSingleTaskTypeAnalysis(
type, typeTasks, level, typeMap, typeNameMap
);
result.get(level).add(analysis);
}
// 对各层级按任务数量降序排序
for (int level = 1; level <= 3; level++) {
List<DimensionAnalysisVO.TaskTypeAnalysis> levelList = result.get(level);
levelList.sort((a, b) -> {
Integer totalA = a.getTotalTasks() != null ? a.getTotalTasks() : 0;
Integer totalB = b.getTotalTasks() != null ? b.getTotalTasks() : 0;
return totalB.compareTo(totalA);
});
}
log.info("任务类型分析完成: 一级={}, 二级={}, 三级={}",
result.get(1).size(), result.get(2).size(), result.get(3).size());
return result;
}
/**
* 计算任务类型的层级
* @param typeId 类型ID
* @param typeMap 类型映射
* @return 层级(1/2/3)
*/
private int calculateTypeLevel(Long typeId, Map<Long, DemandType> typeMap) {
DemandType type = typeMap.get(typeId);
if (type == null) {
return 1;
}
int level = 1;
Long parentId = type.getParentId();
while (parentId != null && level < 3) {
level++;
DemandType parent = typeMap.get(parentId);
if (parent == null) {
break;
}
parentId = parent.getParentId();
}
return level;
}
/**
* 构建单个任务类型的分析数据
*/
private DimensionAnalysisVO.TaskTypeAnalysis buildSingleTaskTypeAnalysis(
DemandType type,
List<Task> typeTasks,
int level,
Map<Long, DemandType> typeMap,
Map<Long, String> typeNameMap) {
DimensionAnalysisVO.TaskTypeAnalysis analysis = new DimensionAnalysisVO.TaskTypeAnalysis();
analysis.setTypeId(type.getId());
analysis.setTypeName(type.getName() != null ? type.getName() : "未知");
analysis.setParentId(type.getParentId());
analysis.setLevel(level);
analysis.setUnitScore(type.getUnitScore());
analysis.setEstimatedHours(type.getEstimatedHours());
// 设置父级名称
if (type.getParentId() != null) {
analysis.setParentName(typeNameMap.getOrDefault(type.getParentId(), "未知"));
}
// 统计任务数量
analysis.setTotalTasks(typeTasks.size());
long completedCount = typeTasks.stream()
.filter(t -> "completed".equals(t.getStatus()))
.count();
analysis.setCompletedTasks((int) completedCount);
long inProgressCount = typeTasks.stream()
.filter(t -> "in_progress".equals(t.getStatus()))
.count();
analysis.setInProgressTasks((int) inProgressCount);
long pendingCount = typeTasks.stream()
.filter(t -> t.getStatus() != null && t.getStatus().contains("pending"))
.count();
analysis.setPendingTasks((int) pendingCount);
// 计算总数量
int totalQuantity = typeTasks.stream()
.filter(t -> t.getQuantity() != null)
.mapToInt(Task::getQuantity)
.sum();
analysis.setTotalQuantity(totalQuantity);
// 计算总积分(优先使用 scoreReward)
BigDecimal totalScore = typeTasks.stream()
.map(t -> {
if (t.getScoreReward() != null && t.getScoreReward().compareTo(BigDecimal.ZERO) > 0) {
return t.getScoreReward();
}
return t.getScore() != null ? t.getScore() : BigDecimal.ZERO;
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
analysis.setTotalScore(totalScore);
// 计算平均积分
if (typeTasks.size() > 0) {
analysis.setAvgScore(totalScore.divide(BigDecimal.valueOf(typeTasks.size()), 2, RoundingMode.HALF_UP));
} else {
analysis.setAvgScore(BigDecimal.ZERO);
}
// 计算完成率
if (typeTasks.size() > 0) {
BigDecimal completionRate = BigDecimal.valueOf(completedCount)
.divide(BigDecimal.valueOf(typeTasks.size()), 4, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100));
analysis.setCompletionRate(completionRate);
} else {
analysis.setCompletionRate(BigDecimal.ZERO);
}
// 计算准时率(已完成任务中准时完成的比例)
if (completedCount > 0) {
long onTimeCount = typeTasks.stream()
.filter(t -> "completed".equals(t.getStatus()))
.filter(t -> {
if (t.getDeadline() == null || t.getActualEndTime() == null) {
return true; // 没有截止时间或完成时间的视为准时
}
return !t.getActualEndTime().isAfter(t.getDeadline());
})
.count();
analysis.setOnTimeRate((double) onTimeCount / completedCount * 100);
} else {
analysis.setOnTimeRate(0.0);
}
// 计算平均完成时长(小时)
List<Task> completedTasks = typeTasks.stream()
.filter(t -> "completed".equals(t.getStatus())
&& t.getActualStartTime() != null
&& t.getActualEndTime() != null)
.collect(Collectors.toList());
if (!completedTasks.isEmpty()) {
double totalHours = completedTasks.stream()
.mapToDouble(t -> Duration.between(t.getActualStartTime(), t.getActualEndTime()).toMinutes() / 60.0)
.sum();
analysis.setAvgCompletionHours(totalHours / completedTasks.size());
} else {
analysis.setAvgCompletionHours(0.0);
}
return analysis;
}
}

View File

@ -42,6 +42,21 @@ public class DimensionAnalysisVO {
*/
private List<PlatformAnalysis> platformAnalysis;
/**
* 任务类型维度分析(一级分类)
*/
private List<TaskTypeAnalysis> taskTypeAnalysis;
/**
* 任务类型维度分析(二级分类)
*/
private List<TaskTypeAnalysis> taskTypeLevel2Analysis;
/**
* 任务类型维度分析(三级分类)
*/
private List<TaskTypeAnalysis> taskTypeLevel3Analysis;
/**
* 类目分析数据
*/
@ -170,4 +185,31 @@ public class DimensionAnalysisVO {
private Integer demandDeptCount; // 需求部门数量
private Integer receivingDeptCount; // 接单部门数量
}
/**
* 任务类型分析数据
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class TaskTypeAnalysis {
private Long typeId; // 类型ID
private String typeName; // 类型名称
private Long parentId; // 父级ID
private String parentName; // 父级名称
private Integer level; // 层级(1=一级,2=二级,3=三级)
private Integer totalTasks; // 总任务数
private Integer completedTasks; // 已完成任务数
private Integer inProgressTasks; // 进行中任务数
private Integer pendingTasks; // 待接单任务数
private Integer totalQuantity; // 总数量
private BigDecimal totalScore; // 总积分
private BigDecimal avgScore; // 平均积分
private BigDecimal completionRate; // 完成率
private Double onTimeRate; // 准时率
private Double avgCompletionHours; // 平均完成时长(小时)
private BigDecimal unitScore; // 单价分值
private BigDecimal estimatedHours; // 预估工时
}
}

View File

@ -465,12 +465,36 @@ export interface PlatformAnalysis {
totalQuantity: number;
}
// 任务类型分析
export interface TaskTypeAnalysis {
typeId: number;
typeName: string;
parentId: number | null;
parentName: string | null;
level: number;
totalTasks: number;
completedTasks: number;
inProgressTasks: number;
pendingTasks: number;
totalQuantity: number;
totalScore: number;
avgScore: number;
completionRate: number;
onTimeRate: number;
avgCompletionHours: number;
unitScore: number;
estimatedHours: number;
}
export interface DimensionAnalysis {
categoryAnalysis: CategoryAnalysis[];
brandAnalysis: BrandAnalysis[];
receivingDeptAnalysis: DeptAnalysis[];
demandDeptAnalysis: DeptAnalysis[];
platformAnalysis: PlatformAnalysis[];
taskTypeAnalysis: TaskTypeAnalysis[];
taskTypeLevel2Analysis: TaskTypeAnalysis[];
taskTypeLevel3Analysis: TaskTypeAnalysis[];
}
/**
@ -499,7 +523,10 @@ export const getDimensionAnalysis = async (
brandAnalysis: [],
receivingDeptAnalysis: [],
demandDeptAnalysis: [],
platformAnalysis: []
platformAnalysis: [],
taskTypeAnalysis: [],
taskTypeLevel2Analysis: [],
taskTypeLevel3Analysis: []
};
};

View File

@ -579,6 +579,138 @@
</template>
</a-table>
</div>
<!-- 任务类型分析(一级分类) -->
<div class="table-card full-width" style="margin-top: 16px;">
<div class="table-header">
<h3 class="chart-title">任务类型分析(一级分类)</h3>
</div>
<a-table
:columns="taskTypeColumns"
:data-source="dimensionData.taskTypeAnalysis"
:pagination="{ pageSize: 10 }"
:scroll="{ x: 1200 }"
row-key="typeId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'avgScore'">
<span class="score-value">{{ formatScore(record.avgScore) }}</span>
</template>
<template v-else-if="column.key === 'unitScore'">
<span class="score-value">{{ formatScore(record.unitScore) }}</span>
</template>
<template v-else-if="column.key === 'completionRate'">
<span :class="record.completionRate >= 80 ? 'text-success' : record.completionRate >= 50 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.completionRate) }}%
</span>
</template>
<template v-else-if="column.key === 'onTimeRate'">
<span :class="record.onTimeRate >= 90 ? 'text-success' : record.onTimeRate >= 70 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.onTimeRate) }}%
</span>
</template>
<template v-else-if="column.key === 'avgCompletionHours'">
<span>{{ record.avgCompletionHours ? formatScore(record.avgCompletionHours) + 'h' : '-' }}</span>
</template>
<template v-else-if="column.key === 'estimatedHours'">
<span>{{ record.estimatedHours ? formatScore(record.estimatedHours) + 'h' : '-' }}</span>
</template>
</template>
</a-table>
</div>
<!-- 任务类型分析(二级分类) -->
<div class="table-card full-width" style="margin-top: 16px;">
<div class="table-header">
<h3 class="chart-title">任务类型分析(二级分类)</h3>
</div>
<a-table
:columns="taskTypeLevel2Columns"
:data-source="dimensionData.taskTypeLevel2Analysis"
:pagination="{ pageSize: 10 }"
:scroll="{ x: 1300 }"
row-key="typeId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'avgScore'">
<span class="score-value">{{ formatScore(record.avgScore) }}</span>
</template>
<template v-else-if="column.key === 'unitScore'">
<span class="score-value">{{ formatScore(record.unitScore) }}</span>
</template>
<template v-else-if="column.key === 'completionRate'">
<span :class="record.completionRate >= 80 ? 'text-success' : record.completionRate >= 50 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.completionRate) }}%
</span>
</template>
<template v-else-if="column.key === 'onTimeRate'">
<span :class="record.onTimeRate >= 90 ? 'text-success' : record.onTimeRate >= 70 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.onTimeRate) }}%
</span>
</template>
<template v-else-if="column.key === 'avgCompletionHours'">
<span>{{ record.avgCompletionHours ? formatScore(record.avgCompletionHours) + 'h' : '-' }}</span>
</template>
<template v-else-if="column.key === 'estimatedHours'">
<span>{{ record.estimatedHours ? formatScore(record.estimatedHours) + 'h' : '-' }}</span>
</template>
</template>
</a-table>
</div>
<!-- 任务类型分析(三级分类) -->
<div class="table-card full-width" style="margin-top: 16px;">
<div class="table-header">
<h3 class="chart-title">任务类型分析(三级分类)</h3>
</div>
<a-table
:columns="taskTypeLevel3Columns"
:data-source="dimensionData.taskTypeLevel3Analysis"
:pagination="{ pageSize: 10 }"
:scroll="{ x: 1400 }"
row-key="typeId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'avgScore'">
<span class="score-value">{{ formatScore(record.avgScore) }}</span>
</template>
<template v-else-if="column.key === 'unitScore'">
<span class="score-value">{{ formatScore(record.unitScore) }}</span>
</template>
<template v-else-if="column.key === 'completionRate'">
<span :class="record.completionRate >= 80 ? 'text-success' : record.completionRate >= 50 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.completionRate) }}%
</span>
</template>
<template v-else-if="column.key === 'onTimeRate'">
<span :class="record.onTimeRate >= 90 ? 'text-success' : record.onTimeRate >= 70 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.onTimeRate) }}%
</span>
</template>
<template v-else-if="column.key === 'avgCompletionHours'">
<span>{{ record.avgCompletionHours ? formatScore(record.avgCompletionHours) + 'h' : '-' }}</span>
</template>
<template v-else-if="column.key === 'estimatedHours'">
<span>{{ record.estimatedHours ? formatScore(record.estimatedHours) + 'h' : '-' }}</span>
</template>
</template>
</a-table>
</div>
</div>
</a-tab-pane>
@ -810,6 +942,7 @@ import {
type DelayDeductionItem,
type ProgressDeductionItem,
type DimensionAnalysis,
type TaskTypeAnalysis,
type PrimaryDeptStatistics,
type PrimaryDeptRanking,
type BrandTaskDistribution,
@ -916,7 +1049,10 @@ const dimensionData = reactive<DimensionAnalysis>({
brandAnalysis: [],
receivingDeptAnalysis: [],
demandDeptAnalysis: [],
platformAnalysis: []
platformAnalysis: [],
taskTypeAnalysis: [],
taskTypeLevel2Analysis: [],
taskTypeLevel3Analysis: []
})
// 一级需求部门统计数据
@ -1163,6 +1299,59 @@ const platformColumns = [
{ title: '总积分', key: 'totalScore', align: 'center', width: 100 }
]
// 任务类型分析列(一级分类)
const taskTypeColumns = [
{ title: '类型名称', dataIndex: 'typeName', key: 'typeName', ellipsis: true, width: 150 },
{ title: '任务数', dataIndex: 'totalTasks', key: 'totalTasks', align: 'center', width: 80 },
{ title: '总数量', dataIndex: 'totalQuantity', key: 'totalQuantity', align: 'center', width: 80 },
{ title: '已完成', dataIndex: 'completedTasks', key: 'completedTasks', align: 'center', width: 80 },
{ title: '进行中', dataIndex: 'inProgressTasks', key: 'inProgressTasks', align: 'center', width: 80 },
{ title: '待接单', dataIndex: 'pendingTasks', key: 'pendingTasks', align: 'center', width: 80 },
{ title: '完成率', key: 'completionRate', align: 'center', width: 90 },
{ title: '准时率', key: 'onTimeRate', align: 'center', width: 90 },
{ title: '总积分', key: 'totalScore', align: 'center', width: 100 },
{ title: '平均积分', key: 'avgScore', align: 'center', width: 100 },
{ title: '单价分值', key: 'unitScore', align: 'center', width: 100 },
{ title: '预估工时', key: 'estimatedHours', align: 'center', width: 100 },
{ title: '平均完成时长', key: 'avgCompletionHours', align: 'center', width: 120 }
]
// 任务类型分析列(二级分类,带父级名称)
const taskTypeLevel2Columns = [
{ title: '父级分类', dataIndex: 'parentName', key: 'parentName', ellipsis: true, width: 120 },
{ title: '类型名称', dataIndex: 'typeName', key: 'typeName', ellipsis: true, width: 150 },
{ title: '任务数', dataIndex: 'totalTasks', key: 'totalTasks', align: 'center', width: 80 },
{ title: '总数量', dataIndex: 'totalQuantity', key: 'totalQuantity', align: 'center', width: 80 },
{ title: '已完成', dataIndex: 'completedTasks', key: 'completedTasks', align: 'center', width: 80 },
{ title: '进行中', dataIndex: 'inProgressTasks', key: 'inProgressTasks', align: 'center', width: 80 },
{ title: '待接单', dataIndex: 'pendingTasks', key: 'pendingTasks', align: 'center', width: 80 },
{ title: '完成率', key: 'completionRate', align: 'center', width: 90 },
{ title: '准时率', key: 'onTimeRate', align: 'center', width: 90 },
{ title: '总积分', key: 'totalScore', align: 'center', width: 100 },
{ title: '平均积分', key: 'avgScore', align: 'center', width: 100 },
{ title: '单价分值', key: 'unitScore', align: 'center', width: 100 },
{ title: '预估工时', key: 'estimatedHours', align: 'center', width: 100 },
{ title: '平均完成时长', key: 'avgCompletionHours', align: 'center', width: 120 }
]
// 任务类型分析列(三级分类,带父级名称)
const taskTypeLevel3Columns = [
{ title: '父级分类', dataIndex: 'parentName', key: 'parentName', ellipsis: true, width: 120 },
{ title: '类型名称', dataIndex: 'typeName', key: 'typeName', ellipsis: true, width: 150 },
{ title: '任务数', dataIndex: 'totalTasks', key: 'totalTasks', align: 'center', width: 80 },
{ title: '总数量', dataIndex: 'totalQuantity', key: 'totalQuantity', align: 'center', width: 80 },
{ title: '已完成', dataIndex: 'completedTasks', key: 'completedTasks', align: 'center', width: 80 },
{ title: '进行中', dataIndex: 'inProgressTasks', key: 'inProgressTasks', align: 'center', width: 80 },
{ title: '待接单', dataIndex: 'pendingTasks', key: 'pendingTasks', align: 'center', width: 80 },
{ title: '完成率', key: 'completionRate', align: 'center', width: 90 },
{ title: '准时率', key: 'onTimeRate', align: 'center', width: 90 },
{ title: '总积分', key: 'totalScore', align: 'center', width: 100 },
{ title: '平均积分', key: 'avgScore', align: 'center', width: 100 },
{ title: '单价分值', key: 'unitScore', align: 'center', width: 100 },
{ title: '预估工时', key: 'estimatedHours', align: 'center', width: 100 },
{ title: '平均完成时长', key: 'avgCompletionHours', align: 'center', width: 120 }
]
// 一级需求部门排名列
const primaryDeptColumns = [
{ title: '排名', key: 'rank', dataIndex: 'rank', align: 'center', width: 60 },