feat:树形数据
This commit is contained in:
@@ -161,7 +161,7 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> impl
|
|||||||
ProjectRequestVo.RequestDescription req = new ProjectRequestVo.RequestDescription();
|
ProjectRequestVo.RequestDescription req = new ProjectRequestVo.RequestDescription();
|
||||||
BeanUtil.copyProperties(detail, req);
|
BeanUtil.copyProperties(detail, req);
|
||||||
req.setDays(CommonTimeFormatUtil.calculateDaysBetween(req.getStartTime(), req.getEndTime()));
|
req.setDays(CommonTimeFormatUtil.calculateDaysBetween(req.getStartTime(), req.getEndTime()));
|
||||||
if (ObjectUtil.isEmpty(req.getParentDetailId())) {
|
if ("0".equals(req.getParentDetailId())) {
|
||||||
requestDescriptions.add(req);
|
requestDescriptions.add(req);
|
||||||
}
|
}
|
||||||
for (StageDetail stageDetail : details) {
|
for (StageDetail stageDetail : details) {
|
||||||
|
|||||||
@@ -16,24 +16,22 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import org.nl.pmm.modular.stagedetail.entity.vo.TreeStageDetailVo;
|
||||||
|
import org.nl.pmm.modular.stagedetail.param.*;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.nl.common.annotation.CommonLog;
|
import org.nl.common.annotation.CommonLog;
|
||||||
import org.nl.common.pojo.CommonResult;
|
import org.nl.common.pojo.CommonResult;
|
||||||
import org.nl.pmm.modular.stagedetail.entity.StageDetail;
|
import org.nl.pmm.modular.stagedetail.entity.StageDetail;
|
||||||
import org.nl.pmm.modular.stagedetail.param.StageDetailAddParam;
|
|
||||||
import org.nl.pmm.modular.stagedetail.param.StageDetailEditParam;
|
|
||||||
import org.nl.pmm.modular.stagedetail.param.StageDetailIdParam;
|
|
||||||
import org.nl.pmm.modular.stagedetail.param.StageDetailPageParam;
|
|
||||||
import org.nl.pmm.modular.stagedetail.service.StageDetailService;
|
import org.nl.pmm.modular.stagedetail.service.StageDetailService;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目阶段明细控制器
|
* 项目阶段明细控制器
|
||||||
@@ -120,4 +118,24 @@ public class StageDetailController {
|
|||||||
public CommonResult<StageDetail> detail(@Valid StageDetailIdParam stageDetailIdParam) {
|
public CommonResult<StageDetail> detail(@Valid StageDetailIdParam stageDetailIdParam) {
|
||||||
return CommonResult.data(stageDetailService.detail(stageDetailIdParam));
|
return CommonResult.data(stageDetailService.detail(stageDetailIdParam));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询基础类型
|
||||||
|
* @param detailTreeParam
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping("/pmm/stagedetail/loadClass")
|
||||||
|
public CommonResult<List<TreeStageDetailVo>> loadClass(StageDetailTreeParam detailTreeParam) {
|
||||||
|
return CommonResult.data(stageDetailService.loadClass(detailTreeParam));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前层级以及所有父类层级
|
||||||
|
* @param detailTreeParam
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping("/pmm/stagedetail/superior")
|
||||||
|
public CommonResult<List<TreeStageDetailVo>> superior(StageDetailTreeParam detailTreeParam) {
|
||||||
|
return CommonResult.data(stageDetailService.getSuperior(detailTreeParam));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package org.nl.pmm.modular.stagedetail.entity.vo;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author: lyd
|
||||||
|
* @date: 2026/1/20
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TreeStageDetailVo {
|
||||||
|
/** 明细id */
|
||||||
|
private String detailId;
|
||||||
|
|
||||||
|
/** 阶段id */
|
||||||
|
private String stageId;
|
||||||
|
|
||||||
|
/** 父明细id */
|
||||||
|
private String parentDetailId;
|
||||||
|
|
||||||
|
/** 序号 */
|
||||||
|
private String seq;
|
||||||
|
|
||||||
|
/** 功能模块 */
|
||||||
|
private String modelName;
|
||||||
|
|
||||||
|
/** 功能明细 */
|
||||||
|
private String modelDetail;
|
||||||
|
|
||||||
|
/** 是否为叶子节点 */
|
||||||
|
private Boolean isLeaf;
|
||||||
|
|
||||||
|
/** 子节点 */
|
||||||
|
private List<TreeStageDetailVo> children;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -14,6 +14,10 @@ package org.nl.pmm.modular.stagedetail.mapper;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import org.nl.pmm.modular.stagedetail.entity.StageDetail;
|
import org.nl.pmm.modular.stagedetail.entity.StageDetail;
|
||||||
|
import org.nl.pmm.modular.stagedetail.entity.vo.TreeStageDetailVo;
|
||||||
|
import org.nl.pmm.modular.stagedetail.param.StageDetailTreeParam;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目阶段明细Mapper接口
|
* 项目阶段明细Mapper接口
|
||||||
@@ -22,4 +26,8 @@ import org.nl.pmm.modular.stagedetail.entity.StageDetail;
|
|||||||
* @date 2025/11/17 20:34
|
* @date 2025/11/17 20:34
|
||||||
**/
|
**/
|
||||||
public interface StageDetailMapper extends BaseMapper<StageDetail> {
|
public interface StageDetailMapper extends BaseMapper<StageDetail> {
|
||||||
|
/**
|
||||||
|
* 树形加载
|
||||||
|
*/
|
||||||
|
List<TreeStageDetailVo> loadClass(StageDetailTreeParam detailTreeParam);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,27 @@
|
|||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="org.nl.pmm.modular.stagedetail.mapper.StageDetailMapper">
|
<mapper namespace="org.nl.pmm.modular.stagedetail.mapper.StageDetailMapper">
|
||||||
|
|
||||||
|
<select id="loadClass" parameterType="org.nl.pmm.modular.stagedetail.param.StageDetailTreeParam"
|
||||||
|
resultType="org.nl.pmm.modular.stagedetail.entity.vo.TreeStageDetailVo">
|
||||||
|
select
|
||||||
|
detail_id as detailId,
|
||||||
|
stage_id as stageId,
|
||||||
|
parent_detail_id as parentDetailId,
|
||||||
|
seq,
|
||||||
|
model_name as modelName,
|
||||||
|
model_detail as modelDetail,
|
||||||
|
case
|
||||||
|
when exists(select 1 from pmm_stage_detail psd where psd.parent_detail_id = pmm_stage_detail.detail_id)
|
||||||
|
then false else true end as isLeaf
|
||||||
|
from pmm_stage_detail
|
||||||
|
where stage_id = #{stageId}
|
||||||
|
<if test="parentDetailId == null or parentDetailId == ''">
|
||||||
|
and parent_detail_id is null
|
||||||
|
</if>
|
||||||
|
<if test="parentDetailId != null and parentDetailId != ''">
|
||||||
|
and parent_detail_id = #{parentDetailId}
|
||||||
|
</if>
|
||||||
|
order by seq, detail_id
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -12,13 +12,14 @@
|
|||||||
*/
|
*/
|
||||||
package org.nl.pmm.modular.stagedetail.param;
|
package org.nl.pmm.modular.stagedetail.param;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.fhs.core.trans.anno.Trans;
|
||||||
|
import com.fhs.core.trans.constant.TransType;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,4 +72,36 @@ public class StageDetailAddParam {
|
|||||||
@Schema(description = "结束时间")
|
@Schema(description = "结束时间")
|
||||||
private Date endTime;
|
private Date endTime;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
/** 创建人 */
|
||||||
|
@Schema(description = "创建人")
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
@Trans(type = TransType.RPC, targetClassName = "org.nl.sys.modular.user.entity.SysUser", fields = "name", alias = "createUser", ref = "createUserName")
|
||||||
|
private String createUser;
|
||||||
|
|
||||||
|
/** 创建人名称 */
|
||||||
|
@Schema(description = "创建人名称")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String createUserName;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
|
/** 更新人 */
|
||||||
|
@Schema(description = "更新人")
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
@Trans(type = TransType.RPC, targetClassName = "org.nl.sys.modular.user.entity.SysUser", fields = "name", alias = "updateUser", ref = "updateUserName")
|
||||||
|
private String updateUser;
|
||||||
|
|
||||||
|
/** 更新人名称 */
|
||||||
|
@Schema(description = "更新人名称")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String updateUserName;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package org.nl.pmm.modular.stagedetail.param;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author: lyd
|
||||||
|
* @date: 2026/1/20
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class StageDetailTreeParam {
|
||||||
|
/** 阶段id */
|
||||||
|
@Schema(description = "阶段id")
|
||||||
|
private String stageId;
|
||||||
|
|
||||||
|
/** 父明细id */
|
||||||
|
@Schema(description = "父明细id")
|
||||||
|
private String parentDetailId;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -15,10 +15,8 @@ package org.nl.pmm.modular.stagedetail.service;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import org.nl.pmm.modular.stagedetail.entity.StageDetail;
|
import org.nl.pmm.modular.stagedetail.entity.StageDetail;
|
||||||
import org.nl.pmm.modular.stagedetail.param.StageDetailAddParam;
|
import org.nl.pmm.modular.stagedetail.entity.vo.TreeStageDetailVo;
|
||||||
import org.nl.pmm.modular.stagedetail.param.StageDetailEditParam;
|
import org.nl.pmm.modular.stagedetail.param.*;
|
||||||
import org.nl.pmm.modular.stagedetail.param.StageDetailIdParam;
|
|
||||||
import org.nl.pmm.modular.stagedetail.param.StageDetailPageParam;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -79,4 +77,13 @@ public interface StageDetailService extends IService<StageDetail> {
|
|||||||
StageDetail queryEntity(String id);
|
StageDetail queryEntity(String id);
|
||||||
|
|
||||||
List<StageDetail> getByStageId(String stageId);
|
List<StageDetail> getByStageId(String stageId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前父类的所有下一子集数据
|
||||||
|
* @param detailTreeParam
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
List<TreeStageDetailVo> loadClass(StageDetailTreeParam detailTreeParam);
|
||||||
|
|
||||||
|
List<TreeStageDetailVo> getSuperior(StageDetailTreeParam detailTreeParam);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import org.nl.pmm.modular.stagedetail.entity.vo.TreeStageDetailVo;
|
||||||
|
import org.nl.pmm.modular.stagedetail.param.*;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.nl.common.enums.CommonSortOrderEnum;
|
import org.nl.common.enums.CommonSortOrderEnum;
|
||||||
@@ -27,10 +29,6 @@ import org.nl.common.exception.CommonException;
|
|||||||
import org.nl.common.page.CommonPageRequest;
|
import org.nl.common.page.CommonPageRequest;
|
||||||
import org.nl.pmm.modular.stagedetail.entity.StageDetail;
|
import org.nl.pmm.modular.stagedetail.entity.StageDetail;
|
||||||
import org.nl.pmm.modular.stagedetail.mapper.StageDetailMapper;
|
import org.nl.pmm.modular.stagedetail.mapper.StageDetailMapper;
|
||||||
import org.nl.pmm.modular.stagedetail.param.StageDetailAddParam;
|
|
||||||
import org.nl.pmm.modular.stagedetail.param.StageDetailEditParam;
|
|
||||||
import org.nl.pmm.modular.stagedetail.param.StageDetailIdParam;
|
|
||||||
import org.nl.pmm.modular.stagedetail.param.StageDetailPageParam;
|
|
||||||
import org.nl.pmm.modular.stagedetail.service.StageDetailService;
|
import org.nl.pmm.modular.stagedetail.service.StageDetailService;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -100,4 +98,21 @@ public class StageDetailServiceImpl extends ServiceImpl<StageDetailMapper, Stage
|
|||||||
.orderByAsc(StageDetail::getSeq);
|
.orderByAsc(StageDetail::getSeq);
|
||||||
return list(lam);
|
return list(lam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TreeStageDetailVo> loadClass(StageDetailTreeParam detailTreeParam) {
|
||||||
|
return this.baseMapper.loadClass(detailTreeParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前对应父节点的所有子节点,以及所有的父节点数据
|
||||||
|
* <p>例:当前是顶层,则返回所有当前stageId的顶层(parentDetailId),
|
||||||
|
* 如果当前是第三层,则获取当前节点的所有同父节点的数据以及所有第二层和所有第一层,并且保持上下级关系,子节点放到children字段中。
|
||||||
|
* @param detailTreeParam
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<TreeStageDetailVo> getSuperior(StageDetailTreeParam detailTreeParam) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
312
nl-vue/src/views/pmm/project/request/addRequestDialog.vue
Normal file
312
nl-vue/src/views/pmm/project/request/addRequestDialog.vue
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
<template>
|
||||||
|
<xn-form-container
|
||||||
|
:title="formData.detailId ? '编辑项目需求' : '增加项目需求'"
|
||||||
|
:width="700"
|
||||||
|
v-model:open="open"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
@close="onClose"
|
||||||
|
>
|
||||||
|
<a-form
|
||||||
|
ref="formRef"
|
||||||
|
class="request-form"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
layout="horizontal"
|
||||||
|
:label-col="{ flex: '96px' }"
|
||||||
|
:wrapper-col="{ flex: 1 }"
|
||||||
|
:label-align="'right'"
|
||||||
|
>
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="功能模块:" name="modelName">
|
||||||
|
<a-input v-model:value="formData.modelName" placeholder="请输入功能模块" allow-clear :maxlength="50" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="功能明细:" name="modelDetail">
|
||||||
|
<a-input v-model:value="formData.modelDetail" placeholder="请输入功能明细" allow-clear :maxlength="50" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="24">
|
||||||
|
<a-form-item label="描述:" name="modelDescription">
|
||||||
|
<a-textarea
|
||||||
|
v-model:value="formData.modelDescription"
|
||||||
|
placeholder="请输入描述"
|
||||||
|
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||||
|
:maxlength="500"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="优先级:" name="priority">
|
||||||
|
<a-select v-model:value="formData.priority" placeholder="请选择优先级" allow-clear>
|
||||||
|
<a-select-option v-for="item in priorityOptions" :key="item.value" :value="item.value">
|
||||||
|
{{ item.label }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="计划天数:" name="days">
|
||||||
|
<a-input-number v-model:value="formData.days" :min="1" :precision="0" style="width: 100%" placeholder="请输入计划天数" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="序号:" name="seq">
|
||||||
|
<a-input-number v-model:value="formData.seq" :min="1" :precision="0" style="width: 100%" placeholder="请输入序号" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="计划开始:" name="startTime">
|
||||||
|
<a-date-picker v-model:value="formData.startTime" value-format="YYYY-MM-DD" style="width: 100%" placeholder="请选择开始日期" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="计划结束:" name="endTime">
|
||||||
|
<a-date-picker v-model:value="formData.endTime" value-format="YYYY-MM-DD" style="width: 100%" placeholder="自动计算" disabled />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="顶级类目:" name="isTop">
|
||||||
|
<a-radio-group v-model:value="formData.isTop" @change="onTopChange">
|
||||||
|
<a-radio :value="true">是</a-radio>
|
||||||
|
<a-radio :value="false">否</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12" v-if="formData.isTop === false">
|
||||||
|
<a-form-item label="上级类目:" name="parentDetailId">
|
||||||
|
<a-tree-select
|
||||||
|
v-model:value="formData.parentDetailId"
|
||||||
|
:tree-data="parentTreeData"
|
||||||
|
placeholder="请选择上级类目"
|
||||||
|
allow-clear
|
||||||
|
tree-default-expand-all
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="24">
|
||||||
|
<a-form-item label="备注:" name="remark">
|
||||||
|
<a-textarea
|
||||||
|
v-model:value="formData.remark"
|
||||||
|
placeholder="请输入备注"
|
||||||
|
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||||
|
:maxlength="200"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form>
|
||||||
|
<template #footer>
|
||||||
|
<a-button style="margin-right: 8px" @click="onClose">关闭</a-button>
|
||||||
|
<a-button type="primary" @click="onSubmit" :loading="submitLoading">保存</a-button>
|
||||||
|
</template>
|
||||||
|
</xn-form-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="addRequestDialog">
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
import { required } from '@/utils/formRules'
|
||||||
|
import projectApi from '@/api/pmm/projectApi'
|
||||||
|
import stageDetailApi from '@/api/pmm/stageDetailApi'
|
||||||
|
|
||||||
|
const open = ref(false)
|
||||||
|
const emit = defineEmits({ successful: null })
|
||||||
|
const formRef = ref()
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const parentTreeData = ref([])
|
||||||
|
|
||||||
|
const priorityOptions = [
|
||||||
|
{ value: 'P0', label: 'P0(紧急)' },
|
||||||
|
{ value: 'P1', label: 'P1(高)' },
|
||||||
|
{ value: 'P2', label: 'P2(中)' },
|
||||||
|
{ value: 'P3', label: 'P3(低)' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const defaultForm = () => ({
|
||||||
|
detailId: '',
|
||||||
|
projectId: '',
|
||||||
|
stageId: '',
|
||||||
|
modelName: '',
|
||||||
|
modelDetail: '',
|
||||||
|
modelDescription: '',
|
||||||
|
seq: 99,
|
||||||
|
priority: undefined,
|
||||||
|
remark: '',
|
||||||
|
days: undefined,
|
||||||
|
startTime: '',
|
||||||
|
endTime: '',
|
||||||
|
isTop: true,
|
||||||
|
parentDetailId: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const formData = ref(defaultForm())
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
modelDetail: [required('请输入功能明细')],
|
||||||
|
modelDescription: [required('请输入描述')],
|
||||||
|
priority: [required('请选择优先级')],
|
||||||
|
seq: [required('请输入序号')],
|
||||||
|
days: [required('请输入计划天数')],
|
||||||
|
startTime: [required('请选择计划开始时间')],
|
||||||
|
parentDetailId: [
|
||||||
|
{
|
||||||
|
validator: async (_rule, value) => {
|
||||||
|
if (formData.value.isTop === false && !value) {
|
||||||
|
return Promise.reject(new Error('请选择上级类目'))
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildTree = (nodes) => {
|
||||||
|
const list = Array.isArray(nodes) ? nodes : []
|
||||||
|
const byId = new Map()
|
||||||
|
const roots = []
|
||||||
|
list.forEach((item) => {
|
||||||
|
const id = item?.detailId
|
||||||
|
if (!id) return
|
||||||
|
const titleParts = []
|
||||||
|
if (item.modelName) titleParts.push(item.modelName)
|
||||||
|
if (item.modelDetail) titleParts.push(item.modelDetail)
|
||||||
|
const title = titleParts.length ? titleParts.join(' - ') : String(id)
|
||||||
|
byId.set(id, { title, value: id, key: id, children: [], raw: item })
|
||||||
|
})
|
||||||
|
byId.forEach((node) => {
|
||||||
|
const parentId = node.raw?.parentDetailId
|
||||||
|
if (parentId && byId.has(parentId)) {
|
||||||
|
byId.get(parentId).children.push(node)
|
||||||
|
} else {
|
||||||
|
roots.push(node)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return roots
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadParentTree = async (projectId) => {
|
||||||
|
if (!projectId) {
|
||||||
|
parentTreeData.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const data = await projectApi.requestDetail({ projectId })
|
||||||
|
const stages = Array.isArray(data) ? data : []
|
||||||
|
const flat = stages.flatMap((s) => (Array.isArray(s?.descriptions) ? s.descriptions : []))
|
||||||
|
parentTreeData.value = buildTree(flat)
|
||||||
|
}
|
||||||
|
|
||||||
|
const recomputeEndTime = () => {
|
||||||
|
const { startTime, days } = formData.value
|
||||||
|
if (!startTime || !days) {
|
||||||
|
formData.value.endTime = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formData.value.endTime = dayjs(startTime).add(Number(days), 'day').format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [formData.value.startTime, formData.value.days],
|
||||||
|
() => {
|
||||||
|
recomputeEndTime()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const onTopChange = () => {
|
||||||
|
if (formData.value.isTop === true) {
|
||||||
|
formData.value.parentDetailId = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开
|
||||||
|
const onOpen = async (payload = {}) => {
|
||||||
|
open.value = true
|
||||||
|
formData.value = Object.assign(defaultForm(), cloneDeep(payload || {}))
|
||||||
|
if (typeof formData.value.isTop !== 'boolean') {
|
||||||
|
formData.value.isTop = !formData.value.parentDetailId
|
||||||
|
}
|
||||||
|
if (formData.value.isTop) {
|
||||||
|
formData.value.parentDetailId = 0
|
||||||
|
}
|
||||||
|
await loadParentTree(formData.value.projectId)
|
||||||
|
recomputeEndTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭
|
||||||
|
const onClose = () => {
|
||||||
|
formRef.value?.resetFields?.()
|
||||||
|
formData.value = defaultForm()
|
||||||
|
parentTreeData.value = []
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
formRef.value
|
||||||
|
.validate()
|
||||||
|
.then(() => {
|
||||||
|
submitLoading.value = true
|
||||||
|
const formDataParam = cloneDeep(formData.value)
|
||||||
|
// 顶级 parentDetailId 置 0
|
||||||
|
if (formDataParam.isTop) {
|
||||||
|
formDataParam.parentDetailId = 0
|
||||||
|
}
|
||||||
|
delete formDataParam.isTop
|
||||||
|
|
||||||
|
// 后端期望可解析的 Date 格式,补全时分秒
|
||||||
|
const formatDateTime = (val) => (val ? dayjs(val).format('YYYY-MM-DD HH:mm:ss') : '')
|
||||||
|
formDataParam.startTime = formatDateTime(formDataParam.startTime)
|
||||||
|
formDataParam.endTime = formatDateTime(formDataParam.endTime)
|
||||||
|
|
||||||
|
stageDetailApi
|
||||||
|
.stageDetailSubmitForm(formDataParam, formDataParam.detailId)
|
||||||
|
.then(() => {
|
||||||
|
onClose()
|
||||||
|
emit('successful')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
submitLoading.value = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
onOpen
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 让横向表单在各种控件(含 textarea)下都保持统一对齐 */
|
||||||
|
.request-form :deep(.ant-form-item-label > label) {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
.request-form :deep(.ant-form-item-control) {
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
.request-form :deep(.ant-form-item-control-input) {
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<template #title>
|
<template #title>
|
||||||
<div class="stage-title-wrapper">
|
<div class="stage-title-wrapper">
|
||||||
<span>第{{ getStageNumber(stage.stageSeq) }}阶段 {{ stage.stageName }}</span>
|
<span>第{{ getStageNumber(stage.stageSeq) }}阶段 {{ stage.stageName }}</span>
|
||||||
<a-button type="primary" class="create-requirement-btn">创建需求</a-button>
|
<a-button type="primary" class="create-requirement-btn" @click="openCreateRequest(stage)">创建需求</a-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<a-table
|
<a-table
|
||||||
@@ -116,16 +116,16 @@
|
|||||||
{{ record.days }}
|
{{ record.days }}
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.dataIndex === 'action'">
|
<template v-if="column.dataIndex === 'action'">
|
||||||
<a-button type="link" size="small" class="action-btn">
|
<a-button type="link" size="small" class="action-btn" @click="openAddSibling(record)">
|
||||||
新增同级
|
新增同级
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="link" size="small" class="action-btn">
|
<a-button type="link" size="small" class="action-btn" @click="openAddChild(record)">
|
||||||
新增子级
|
新增子级
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="link" size="small" class="action-btn">
|
<a-button type="link" size="small" class="action-btn" @click="openEditRequest(record)">
|
||||||
修改需求
|
修改需求
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="link" size="small" class="action-btn">
|
<a-button type="link" size="small" class="action-btn" @click="deleteRequest(record)">
|
||||||
删除需求
|
删除需求
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -183,6 +183,7 @@
|
|||||||
<stage-add-dialog ref="formRef" @successful="fetchDetail"/>
|
<stage-add-dialog ref="formRef" @successful="fetchDetail"/>
|
||||||
<fj-upload-form ref="uploadFormRef" @successful="fetchAttachments"/>
|
<fj-upload-form ref="uploadFormRef" @successful="fetchAttachments"/>
|
||||||
<batch-stage-add-dialog ref="form2Ref" @successful="fetchDetail"/>
|
<batch-stage-add-dialog ref="form2Ref" @successful="fetchDetail"/>
|
||||||
|
<add-request-dialog ref="requestFormRef" @successful="fetchDetail" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -191,8 +192,10 @@
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import projectApi from '@/api/pmm/projectApi'
|
import projectApi from '@/api/pmm/projectApi'
|
||||||
import projectFileApi from '@/api/pmm/projectFileApi'
|
import projectFileApi from '@/api/pmm/projectFileApi'
|
||||||
|
import stageDetailApi from '@/api/pmm/stageDetailApi'
|
||||||
import { Modal, message } from 'ant-design-vue'
|
import { Modal, message } from 'ant-design-vue'
|
||||||
import StageAddDialog from "@/views/pmm/project/request/stageAddDialog.vue";
|
import StageAddDialog from "@/views/pmm/project/request/stageAddDialog.vue";
|
||||||
|
import AddRequestDialog from "@/views/pmm/project/request/addRequestDialog.vue";
|
||||||
import Preview from '@/views/dev/file/preview.vue'
|
import Preview from '@/views/dev/file/preview.vue'
|
||||||
import FjUploadForm from "@/views/pmm/project/request/fjUploadForm.vue";
|
import FjUploadForm from "@/views/pmm/project/request/fjUploadForm.vue";
|
||||||
import BatchStageAddDialog from "@/views/pmm/project/request/batchStageAddDialog.vue";
|
import BatchStageAddDialog from "@/views/pmm/project/request/batchStageAddDialog.vue";
|
||||||
@@ -213,6 +216,7 @@
|
|||||||
const plannedHours = ref(0)
|
const plannedHours = ref(0)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const form2Ref = ref()
|
const form2Ref = ref()
|
||||||
|
const requestFormRef = ref()
|
||||||
const previewRef = ref()
|
const previewRef = ref()
|
||||||
const uploadFormRef = ref()
|
const uploadFormRef = ref()
|
||||||
const previewVisible = ref(false)
|
const previewVisible = ref(false)
|
||||||
@@ -360,8 +364,15 @@ const attachmentList = ref<AttachmentItem[]>([])
|
|||||||
.requestDetail({ projectId: projectId.value })
|
.requestDetail({ projectId: projectId.value })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
stageList.value = data
|
const normalized = data.map((stage: any) => {
|
||||||
plannedHours.value = calculatePlannedHours(data)
|
const descriptions = Array.isArray(stage?.descriptions) ? stage.descriptions : []
|
||||||
|
return {
|
||||||
|
...stage,
|
||||||
|
descriptions: descriptions.map((d: any) => ({ ...d, stageId: stage.stageId }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
stageList.value = normalized
|
||||||
|
plannedHours.value = calculatePlannedHours(normalized)
|
||||||
} else {
|
} else {
|
||||||
stageList.value = []
|
stageList.value = []
|
||||||
message.warning('数据格式错误')
|
message.warning('数据格式错误')
|
||||||
@@ -549,6 +560,69 @@ watch(projectId, () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建需求(不携带 parentDetailId)
|
||||||
|
const openCreateRequest = (stage: any) => {
|
||||||
|
if (!projectId.value) return
|
||||||
|
requestFormRef.value?.onOpen?.({
|
||||||
|
projectId: projectId.value,
|
||||||
|
stageId: stage?.stageId,
|
||||||
|
isTop: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增同级:带过去 parentDetailId(等同 record.parentDetailId)
|
||||||
|
const openAddSibling = (record: any) => {
|
||||||
|
if (!projectId.value) return
|
||||||
|
const parentDetailId = record?.parentDetailId || ''
|
||||||
|
requestFormRef.value?.onOpen?.({
|
||||||
|
projectId: projectId.value,
|
||||||
|
stageId: record?.stageId,
|
||||||
|
isTop: !parentDetailId,
|
||||||
|
parentDetailId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增子级:把自己的 detailId 作为 parentDetailId
|
||||||
|
const openAddChild = (record: any) => {
|
||||||
|
if (!projectId.value) return
|
||||||
|
requestFormRef.value?.onOpen?.({
|
||||||
|
projectId: projectId.value,
|
||||||
|
stageId: record?.stageId,
|
||||||
|
isTop: false,
|
||||||
|
parentDetailId: record?.detailId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
const openEditRequest = (record: any) => {
|
||||||
|
if (!projectId.value) return
|
||||||
|
const parentDetailId = record?.parentDetailId || ''
|
||||||
|
requestFormRef.value?.onOpen?.({
|
||||||
|
...record,
|
||||||
|
projectId: projectId.value,
|
||||||
|
isTop: !parentDetailId,
|
||||||
|
parentDetailId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const deleteRequest = (record: any) => {
|
||||||
|
if (!record?.detailId) return
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除该需求?',
|
||||||
|
content: `需求:${record?.modelDetail || record?.modelName || record?.detailId}`,
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => {
|
||||||
|
const params = [{ detailId: record.detailId }]
|
||||||
|
return stageDetailApi.stageDetailDelete(params).then(() => {
|
||||||
|
message.success('删除成功')
|
||||||
|
fetchDetail()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user