feat: 为任务子类型增加父子层级关系和单价分值字段,支持需求类型大分类筛选

This commit is contained in:
Your Name 2026-01-05 17:47:00 +08:00
parent 1e67845970
commit 1e1aa5872d
5 changed files with 332 additions and 38 deletions

View File

@ -78,4 +78,28 @@ public class TaskSubTypeController {
taskSubTypeService.updateSortOrder(id, sortOrder);
return Result.success();
}
@Operation(summary = "获取树形结构列表(父子层级)")
@GetMapping("/tree")
public Result<List<TaskSubType>> getTreeList() {
return Result.success(taskSubTypeService.getTreeList());
}
@Operation(summary = "获取启用的树形结构")
@GetMapping("/tree/enabled")
public Result<List<TaskSubType>> getEnabledTreeList() {
return Result.success(taskSubTypeService.getEnabledTreeList());
}
@Operation(summary = "获取父级分类列表(需求类型大分类)")
@GetMapping("/parents")
public Result<List<TaskSubType>> getParentSubTypes() {
return Result.success(taskSubTypeService.getParentSubTypes());
}
@Operation(summary = "获取指定父级下的子分类")
@GetMapping("/children/{parentId}")
public Result<List<TaskSubType>> getChildrenByParentId(@PathVariable Long parentId) {
return Result.success(taskSubTypeService.getChildrenByParentId(parentId));
}
}

View File

@ -4,7 +4,9 @@ import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 任务子类型实体
@ -20,6 +22,11 @@ public class TaskSubType implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 父级ID,NULL表示顶级分类(需求类型大分类)
*/
private Long parentId;
/**
* 子类型名称
*/
@ -35,6 +42,11 @@ public class TaskSubType implements Serializable {
*/
private String description;
/**
* 单价分值
*/
private BigDecimal unitPrice;
/**
* 排序
*/
@ -51,6 +63,12 @@ public class TaskSubType implements Serializable {
*/
private Integer checkInventory;
/**
* 子分类列表(非数据库字段)
*/
@TableField(exist = false)
private List<TaskSubType> children;
/**
* 创建时间
*/
@ -135,4 +153,28 @@ public class TaskSubType implements Serializable {
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
public Long getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
this.parentId = parentId;
}
public BigDecimal getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(BigDecimal unitPrice) {
this.unitPrice = unitPrice;
}
public List<TaskSubType> getChildren() {
return children;
}
public void setChildren(List<TaskSubType> children) {
this.children = children;
}
}

View File

@ -10,7 +10,10 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 任务子类型服务
@ -142,4 +145,82 @@ public class TaskSubTypeService {
subType.setSortOrder(sortOrder);
taskSubTypeMapper.updateById(subType);
}
/**
* 获取树形结构的子类型列表
* 返回父子层级结构,便于前端展示和筛选
*/
public List<TaskSubType> getTreeList() {
List<TaskSubType> allSubTypes = taskSubTypeMapper.selectList(
new LambdaQueryWrapper<TaskSubType>()
.orderByAsc(TaskSubType::getSortOrder)
);
// 找出所有顶级分类(parentId为null)
List<TaskSubType> parents = allSubTypes.stream()
.filter(st -> st.getParentId() == null)
.collect(Collectors.toList());
// 按parentId分组子分类
Map<Long, List<TaskSubType>> childrenMap = allSubTypes.stream()
.filter(st -> st.getParentId() != null)
.collect(Collectors.groupingBy(TaskSubType::getParentId));
// 为每个父分类设置子分类
for (TaskSubType parent : parents) {
parent.setChildren(childrenMap.getOrDefault(parent.getId(), new ArrayList<>()));
}
return parents;
}
/**
* 获取所有父级分类(需求类型大分类)
*/
public List<TaskSubType> getParentSubTypes() {
return taskSubTypeMapper.selectList(
new LambdaQueryWrapper<TaskSubType>()
.isNull(TaskSubType::getParentId)
.orderByAsc(TaskSubType::getSortOrder)
);
}
/**
* 获取指定父级下的子分类
*/
public List<TaskSubType> getChildrenByParentId(Long parentId) {
return taskSubTypeMapper.selectList(
new LambdaQueryWrapper<TaskSubType>()
.eq(TaskSubType::getParentId, parentId)
.orderByAsc(TaskSubType::getSortOrder)
);
}
/**
* 获取启用的树形结构
*/
public List<TaskSubType> getEnabledTreeList() {
List<TaskSubType> allSubTypes = taskSubTypeMapper.selectList(
new LambdaQueryWrapper<TaskSubType>()
.eq(TaskSubType::getIsEnabled, 1)
.orderByAsc(TaskSubType::getSortOrder)
);
// 找出所有顶级分类(parentId为null)
List<TaskSubType> parents = allSubTypes.stream()
.filter(st -> st.getParentId() == null)
.collect(Collectors.toList());
// 按parentId分组子分类
Map<Long, List<TaskSubType>> childrenMap = allSubTypes.stream()
.filter(st -> st.getParentId() != null)
.collect(Collectors.groupingBy(TaskSubType::getParentId));
// 为每个父分类设置子分类
for (TaskSubType parent : parents) {
parent.setChildren(childrenMap.getOrDefault(parent.getId(), new ArrayList<>()));
}
return parents;
}
}

View File

@ -63,13 +63,17 @@ export interface TeamScorePool {
export interface TaskSubType {
id: number
parentId?: number | null // 父级ID,null表示顶级分类
name: string
code: string
description?: string
unitPrice?: number // 单价分值
sortOrder?: number
isEnabled?: number
checkInventory?: number // 是否检查库存
createTime?: string
updateTime?: string
children?: TaskSubType[] // 子分类列表
}
/**
@ -311,3 +315,31 @@ export const toggleTaskSubTypeEnabled = (id: number) => {
return request.post<any>(`/task-sub-types/${id}/toggle-enabled`)
}
/**
* 获取树形结构的任务子类型(父子层级)
*/
export const getTaskSubTypeTree = () => {
return request.get<any>('/task-sub-types/tree')
}
/**
* 获取启用的树形结构
*/
export const getEnabledTaskSubTypeTree = () => {
return request.get<any>('/task-sub-types/tree/enabled')
}
/**
* 获取父级分类列表(需求类型大分类)
*/
export const getTaskSubTypeParents = () => {
return request.get<any>('/task-sub-types/parents')
}
/**
* 获取指定父级下的子分类
*/
export const getTaskSubTypeChildren = (parentId: number) => {
return request.get<any>(`/task-sub-types/children/${parentId}`)
}

View File

@ -136,21 +136,47 @@
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">任务子类型管理</h2>
<a-button type="primary" @click="showTaskSubTypeModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加子类型
</a-button>
<a-space>
<a-select
v-model:value="taskSubTypeFilter.parentId"
placeholder="按需求类型筛选"
style="width: 160px"
allow-clear
@change="filterTaskSubTypes"
>
<a-select-option :value="null">全部类型</a-select-option>
<a-select-option v-for="p in taskSubTypeParents" :key="p.id" :value="p.id">
{{ p.name }}
</a-select-option>
</a-select>
<a-button type="primary" @click="showTaskSubTypeModal(null, true)">
<template #icon><Icon name="plus" size="sm" /></template>
添加大分类
</a-button>
<a-button type="primary" @click="showTaskSubTypeModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加子类型
</a-button>
</a-space>
</div>
<a-table
:columns="taskSubTypeColumns"
:data-source="taskSubTypes"
:data-source="filteredTaskSubTypes"
:loading="loading.taskSubTypes"
row-key="id"
:pagination="{ pageSize: 10 }"
:pagination="{ pageSize: 15 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<template v-if="column.key === 'parentName'">
<a-tag v-if="!record.parentId" color="blue">{{ record.name }}</a-tag>
<span v-else>{{ getParentName(record.parentId) }}</span>
</template>
<template v-else-if="column.key === 'checkInventory'">
<a-tag v-if="record.checkInventory === 1" color="orange">是</a-tag>
<span v-else style="color: #999;">否</span>
</template>
<template v-else-if="column.key === 'status'">
<a-switch
:checked="record.isEnabled === 1"
@change="(checked: boolean) => toggleStatus('taskSubType', record.id, checked)"
@ -1178,15 +1204,31 @@
<!-- 任务子类型弹窗 -->
<a-modal
v-model:open="modals.taskSubType"
:title="editingItem ? '编辑任务子类型' : '添加任务子类型'"
:title="taskSubTypeModalTitle"
@ok="saveTaskSubType"
@cancel="closeModal('taskSubType')"
width="560px"
>
<a-form :model="formData.taskSubType" layout="vertical">
<a-form-item label="子类型名称" required>
<a-input v-model:value="formData.taskSubType.name" placeholder="请输入子类型名称(如:新品、优化)" />
<a-form-item v-if="!isAddingParentType" label="所属需求类型(大分类)">
<a-select
v-model:value="formData.taskSubType.parentId"
placeholder="选择大分类(留空则为顶级分类)"
allow-clear
>
<a-select-option v-for="p in taskSubTypeParents" :key="p.id" :value="p.id">
{{ p.name }}
</a-select-option>
</a-select>
<div class="form-tip" style="margin-top: 4px;">
<Icon name="info" size="sm" color="#6B6B6B" style="margin-right: 4px;" />
选择大分类后,此子类型将归属于该分类下
</div>
</a-form-item>
<a-form-item label="子类型编码" required>
<a-form-item :label="isAddingParentType ? '需求类型名称' : '需求类型细分名称'" required>
<a-input v-model:value="formData.taskSubType.name" :placeholder="isAddingParentType ? '请输入大分类名称(如:主图、详情)' : '请输入细分名称(如:制作、部分调整)'" />
</a-form-item>
<a-form-item label="编码" required>
<a-input v-model:value="formData.taskSubType.code" placeholder="留空自动生成">
<template #addonAfter>
<a-button size="small" type="link" @click="autoGenerateTaskSubTypeCode" style="padding: 0 8px;">
@ -1195,25 +1237,42 @@
</template>
</a-input>
</a-form-item>
<a-form-item label="描述">
<a-textarea v-model:value="formData.taskSubType.description" :rows="3" placeholder="请输入描述" />
</a-form-item>
<a-form-item label="排序">
<a-form-item v-if="!isAddingParentType" label="单价分值">
<a-input-number
v-model:value="formData.taskSubType.sortOrder"
v-model:value="formData.taskSubType.unitPrice"
:min="0"
:precision="0"
:step="0.01"
:precision="3"
style="width: 100%"
placeholder="排序值越小越靠前"
placeholder="请输入单价分值(如:0.6)"
/>
</a-form-item>
<a-form-item label="是否检查库存">
<a-switch v-model:checked="formData.taskSubType.checkInventory" />
<div class="form-tip" style="margin-top: 8px;">
<Icon name="info" size="sm" color="#6B6B6B" style="margin-right: 4px;" />
开启后,发布此类型任务时需要输入商品条码并检查库存,库存不足则无法发布任务
</div>
<a-form-item label="描述">
<a-textarea v-model:value="formData.taskSubType.description" :rows="2" placeholder="请输入描述" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="排序">
<a-input-number
v-model:value="formData.taskSubType.sortOrder"
:min="0"
:precision="0"
style="width: 100%"
placeholder="值越小越靠前"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item v-if="!isAddingParentType" label="是否检查库存">
<a-switch v-model:checked="formData.taskSubType.checkInventory" />
</a-form-item>
</a-col>
</a-row>
<a-alert v-if="!isAddingParentType && formData.taskSubType.checkInventory" type="info" show-icon style="margin-top: 8px;">
<template #message>
开启后,发布此类型任务时需要输入商品条码并检查库存,库存不足则无法发布任务
</template>
</a-alert>
</a-form>
</a-modal>
@ -1662,6 +1721,7 @@ import {
getPlatforms, createPlatform, updatePlatform, deletePlatform,
getCategories, createCategory, updateCategory, deleteCategory,
getAllTaskSubTypes, createTaskSubType, updateTaskSubType, deleteTaskSubType, toggleTaskSubTypeEnabled,
getTaskSubTypeParents,
type DemandType, type Brand, type Platform, type Category, type TeamScorePool, type TaskSubType
} from '@/api/config'
import {
@ -1775,6 +1835,9 @@ const modals = reactive({
const demandTypes = ref<DemandType[]>([])
const taskSubTypes = ref<TaskSubType[]>([])
const taskSubTypeParents = ref<TaskSubType[]>([]) // 需求类型大分类列表
const taskSubTypeFilter = reactive({ parentId: null as number | null }) // 筛选条件
const isAddingParentType = ref(false) // 是否正在添加大分类
const brands = ref<Brand[]>([])
const platforms = ref<Platform[]>([])
const categories = ref<Category[]>([])
@ -1789,6 +1852,33 @@ const scoreAdjustForm = reactive<ScoreAdjustPayload>({
})
const rechargeAmount = ref<number>(0)
// 任务子类型相关计算属性和方法
const filteredTaskSubTypes = computed(() => {
if (!taskSubTypeFilter.parentId) {
return taskSubTypes.value
}
return taskSubTypes.value.filter(t =>
t.parentId === taskSubTypeFilter.parentId || t.id === taskSubTypeFilter.parentId
)
})
const taskSubTypeModalTitle = computed(() => {
if (editingItem.value) {
return editingItem.value.parentId ? '编辑需求类型细分' : '编辑需求类型'
}
return isAddingParentType.value ? '添加需求类型(大分类)' : '添加需求类型细分'
})
const getParentName = (parentId: number | null | undefined) => {
if (!parentId) return '-'
const parent = taskSubTypeParents.value.find(p => p.id === parentId)
return parent ? parent.name : '-'
}
const filterTaskSubTypes = () => {
// 筛选器变化时触发重新渲染
}
const userLoading = ref(false)
const usersLoaded = ref(false)
const usersByDept = ref<Record<number, User[]>>({})
@ -2028,13 +2118,15 @@ const demandTypeColumns = [
]
const taskSubTypeColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '子类型名称', dataIndex: 'name', key: 'name' },
{ title: '子类型编码', dataIndex: 'code', key: 'code' },
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
{ title: '状态', key: 'status', width: 100 },
{ title: '操作', key: 'actions', width: 150 }
{ title: 'ID', dataIndex: 'id', key: 'id', width: 60 },
{ title: '需求类型', key: 'parentName', width: 100 },
{ title: '需求类型细分', dataIndex: 'name', key: 'name' },
{ title: '单价分值', dataIndex: 'unitPrice', key: 'unitPrice', width: 100 },
{ title: '编码', dataIndex: 'code', key: 'code', width: 120 },
{ title: '检查库存', key: 'checkInventory', width: 90 },
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 70 },
{ title: '状态', key: 'status', width: 80 },
{ title: '操作', key: 'actions', width: 130 }
]
const brandColumns = [
@ -2109,8 +2201,13 @@ const loadDemandTypes = async () => {
const loadTaskSubTypes = async () => {
loading.taskSubTypes = true
try {
const response = await getAllTaskSubTypes()
taskSubTypes.value = response.data || []
// 同时加载所有子类型和父级分类
const [allResponse, parentsResponse] = await Promise.all([
getAllTaskSubTypes(),
getTaskSubTypeParents()
])
taskSubTypes.value = allResponse.data || []
taskSubTypeParents.value = parentsResponse.data || []
} catch (error: any) {
console.error('加载任务子类型失败:', error)
const errorMsg = error?.response?.data?.message || error?.message || '加载任务子类型失败'
@ -2385,11 +2482,27 @@ const showDemandTypeModal = async (item?: DemandType) => {
modals.demandType = true
}
const showTaskSubTypeModal = (item?: TaskSubType) => {
const showTaskSubTypeModal = (item?: TaskSubType | null, addParent: boolean = false) => {
editingItem.value = item
formData.taskSubType = item
? { ...item, checkInventory: item.checkInventory === 1 }
: { sortOrder: 0, isEnabled: 1, checkInventory: false }
isAddingParentType.value = addParent
if (item) {
// 编辑模式
formData.taskSubType = {
...item,
checkInventory: item.checkInventory === 1,
unitPrice: item.unitPrice || 0
}
} else {
// 新建模式
formData.taskSubType = {
parentId: addParent ? null : undefined, // 如果是添加大分类,parentId为null
sortOrder: 0,
isEnabled: 1,
checkInventory: false,
unitPrice: 0
}
}
modals.taskSubType = true
}
@ -2493,12 +2606,14 @@ const saveDemandType = async () => {
const saveTaskSubType = async () => {
try {
const saveData: Partial<TaskSubType> & { checkInventory?: number } = {
parentId: isAddingParentType.value ? null : (formData.taskSubType.parentId || null),
name: formData.taskSubType.name,
code: formData.taskSubType.code,
description: formData.taskSubType.description,
unitPrice: formData.taskSubType.unitPrice ?? 0,
sortOrder: formData.taskSubType.sortOrder ?? 0,
isEnabled: formData.taskSubType.isEnabled ?? 1,
checkInventory: (formData.taskSubType as any).checkInventory ? 1 : 0
checkInventory: isAddingParentType.value ? 0 : ((formData.taskSubType as any).checkInventory ? 1 : 0)
}
if (editingItem.value) {