feat: 任务类型选择改为三级级联选择

This commit is contained in:
Your Name 2026-01-05 22:18:06 +08:00
parent b20c9e1361
commit 65ed9e4d6d
2 changed files with 157 additions and 21 deletions

View File

@ -112,7 +112,7 @@ public class ConfigService {
}
/**
* 构建需求类型树形结构
* 构建需求类型树形结构(支持多级)
*/
private List<DemandType> buildDemandTypeTree(List<DemandType> allTypes) {
// 获取所有根节点(parentId为null)
@ -120,16 +120,30 @@ public class ConfigService {
.filter(dt -> dt.getParentId() == null)
.collect(java.util.stream.Collectors.toList());
// 为每个根节点填充子节点
// 递归为每个节点填充子节点
for (DemandType root : roots) {
List<DemandType> children = allTypes.stream()
.filter(dt -> root.getId().equals(dt.getParentId()))
.collect(java.util.stream.Collectors.toList());
root.setChildren(children);
fillChildren(root, allTypes);
}
return roots;
}
/**
* 递归填充子节点
*/
private void fillChildren(DemandType parent, List<DemandType> allTypes) {
List<DemandType> children = allTypes.stream()
.filter(dt -> parent.getId().equals(dt.getParentId()))
.collect(java.util.stream.Collectors.toList());
if (!children.isEmpty()) {
parent.setChildren(children);
// 递归填充每个子节点的子节点
for (DemandType child : children) {
fillChildren(child, allTypes);
}
}
}
/**
* 根据ID获取需求类型

View File

@ -56,23 +56,26 @@
class="task-type-item"
>
<div class="task-type-row">
<div class="task-type-select">
<a-select
v-model:value="item.taskTypeId"
placeholder="请选择任务类型"
<div class="task-type-select task-type-cascader">
<a-cascader
v-model:value="item.taskTypePath"
:options="taskTypeCascaderOptions"
placeholder="请选择任务类型(一级→二级→三级)"
size="large"
:loading="loading"
allow-clear
@change="calculateScore"
expand-trigger="hover"
:show-search="{ filter: cascaderFilter }"
@change="(value: any) => onTaskTypeChange(value, item, index)"
>
<a-select-option
v-for="type in taskTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }} ({{ type.unitScore }}分/个, {{ type.estimatedHours }}小时)
</a-select-option>
</a-select>
<template #displayRender="{ labels }">
<span>{{ labels.join(' / ') }}</span>
</template>
</a-cascader>
<div v-if="item.taskTypeId && getTaskTypeInfo(item.taskTypeId)" class="task-type-score-tip">
<span class="score-badge">{{ getTaskTypeInfo(item.taskTypeId)?.unitScore }}分/个</span>
<span class="hour-badge">{{ getTaskTypeInfo(item.taskTypeId)?.estimatedHours }}小时</span>
</div>
</div>
<div class="task-type-quantity">
<a-input-number
@ -545,10 +548,20 @@ const previewUploading = ref(false)
// 任务类型项接口
interface TaskTypeItem {
taskTypeId: number | undefined
taskTypePath?: (string | number)[] // 级联选择路径 [level1Id, level2Id, level3Id]
quantity: number
receiverId?: number // 指定接收人ID
}
// 级联选择器选项接口
interface CascaderOption {
value: number
label: string
children?: CascaderOption[]
unitScore?: number
estimatedHours?: number
}
// 设计师列表缓存 { deptId: designers[] }
const designersCache = reactive<Record<number, any[]>>({})
@ -557,7 +570,7 @@ const formData = reactive({
title: '',
isPipeline: true, // 固定为流水线任务
pipelineMode: 'sequential' as 'sequential' | 'parallel', // 固定为顺序流水线
taskTypes: [{ taskTypeId: undefined, quantity: 1, receiverId: undefined }] as TaskTypeItem[], // 任务类型数组
taskTypes: [{ taskTypeId: undefined, taskTypePath: undefined, quantity: 1, receiverId: undefined }] as TaskTypeItem[], // 任务类型数组
taskSubType: '', // 动态加载,初始为空
brandId: undefined,
platformId: undefined,
@ -651,6 +664,83 @@ const brands = computed(() => configStore.brands)
const platforms = computed(() => configStore.platforms)
const categories = computed(() => configStore.categories)
// 层级任务类型数据
const hierarchicalTaskTypes = ref<any[]>([])
// 加载层级任务类型数据
const loadHierarchicalTaskTypes = async () => {
try {
const response = await configApi.getDemandTypesTree()
if (response && response.data) {
hierarchicalTaskTypes.value = response.data
}
} catch (error) {
console.error('加载层级任务类型失败:', error)
}
}
// 构建级联选择器选项(三级结构)
const taskTypeCascaderOptions = computed<CascaderOption[]>(() => {
const buildOptions = (items: any[]): CascaderOption[] => {
return items.map(item => {
const option: CascaderOption = {
value: item.id,
label: item.name,
unitScore: item.unitScore,
estimatedHours: item.estimatedHours
}
// 如果有子级,递归构建
if (item.children && item.children.length > 0) {
option.children = buildOptions(item.children)
}
return option
})
}
return buildOptions(hierarchicalTaskTypes.value)
})
// 级联选择器搜索过滤
const cascaderFilter = (inputValue: string, path: CascaderOption[]) => {
return path.some(option =>
option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1
)
}
// 任务类型级联选择变更
const onTaskTypeChange = (value: (string | number)[] | undefined, item: TaskTypeItem, index: number) => {
if (value && value.length === 3) {
// 选择了三级分类,设置taskTypeId为三级分类的ID
item.taskTypeId = value[2] as number
item.taskTypePath = value
} else {
// 未选择完整路径
item.taskTypeId = undefined
item.taskTypePath = value
}
calculateScore()
}
// 获取任务类型信息(根据ID从平铺的taskTypes中查找)
const getTaskTypeInfo = (taskTypeId: number) => {
// 从平铺列表中查找
return taskTypes.value.find((t: any) => t.id === taskTypeId)
}
// 从平铺列表中根据ID查找任务类型(递归查找层级数据)
const findTaskTypeById = (id: number, items: any[] = hierarchicalTaskTypes.value): any => {
for (const item of items) {
if (item.id === id) return item
if (item.children) {
const found = findTaskTypeById(id, item.children)
if (found) return found
}
}
return null
}
// 加载设计师列表
const loadDesignersForItem = async (item: TaskTypeItem) => {
if (!item.taskTypeId) return
@ -694,6 +784,7 @@ const getDesignersForItem = (item: TaskTypeItem) => {
const addTaskType = () => {
formData.taskTypes.push({
taskTypeId: undefined,
taskTypePath: undefined,
quantity: 1,
receiverId: undefined
})
@ -1428,7 +1519,8 @@ onMounted(async () => {
loadConfigData(),
loadTeamScore(),
loadTaskSubTypes(),
loadReleaseTypeConfig()
loadReleaseTypeConfig(),
loadHierarchicalTaskTypes()
])
// 检查是否是编辑模式
@ -1678,6 +1770,36 @@ onUnmounted(() => {
flex: 2;
}
.task-type-cascader {
display: flex;
flex-direction: column;
gap: 6px;
:deep(.ant-cascader) {
width: 100%;
}
.task-type-score-tip {
display: flex;
gap: 8px;
font-size: 12px;
.score-badge {
color: $primary;
background: rgba($primary, 0.1);
padding: 2px 8px;
border-radius: 4px;
}
.hour-badge {
color: $text-secondary;
background: rgba(255, 255, 255, 0.05);
padding: 2px 8px;
border-radius: 4px;
}
}
}
.task-type-quantity {
flex: 1;
min-width: 150px;