Merge remote-tracking branch 'origin/develop3' into develop3
This commit is contained in:
commit
f874e9d231
@ -149,7 +149,11 @@ public enum ResponseCode {
|
|||||||
/**
|
/**
|
||||||
* 工作流变量反序列化错误
|
* 工作流变量反序列化错误
|
||||||
*/
|
*/
|
||||||
WORKFLOW_VARIABLE_DESERIALIZE_ERROR(50302, "workflow.variable.deserialize.error");
|
WORKFLOW_VARIABLE_DESERIALIZE_ERROR(50302, "workflow.variable.deserialize.error"),
|
||||||
|
|
||||||
|
// 表单管理相关错误码 (2800-2899)
|
||||||
|
FORM_DEFINITION_NOT_FOUND(2800, "form.definition.not.found"),
|
||||||
|
FORM_DATA_NOT_FOUND(2801, "form.data.not.found");
|
||||||
|
|
||||||
private final int code;
|
private final int code;
|
||||||
private final String messageKey; // 国际化消息key
|
private final String messageKey; // 国际化消息key
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.api;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.api.Response;
|
||||||
|
import com.qqchen.deploy.backend.framework.controller.BaseController;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.FormCategoryDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.query.FormCategoryQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormCategory;
|
||||||
|
import com.qqchen.deploy.backend.workflow.service.IFormCategoryService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类API控制器
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/forms/categories")
|
||||||
|
@Tag(name = "表单分类管理", description = "表单分类管理相关接口")
|
||||||
|
public class FormCategoryApiController extends BaseController<FormCategory, FormCategoryDTO, Long, FormCategoryQuery> {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IFormCategoryService formCategoryService;
|
||||||
|
|
||||||
|
@Operation(summary = "查询所有启用的分类")
|
||||||
|
@GetMapping("/enabled")
|
||||||
|
public Response<List<FormCategoryDTO>> findAllEnabled() {
|
||||||
|
List<FormCategoryDTO> result = formCategoryService.findAllEnabled();
|
||||||
|
return Response.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void exportData(jakarta.servlet.http.HttpServletResponse response, java.util.List<FormCategoryDTO> data) {
|
||||||
|
// 暂不支持导出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.api;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.api.Response;
|
||||||
|
import com.qqchen.deploy.backend.framework.controller.BaseController;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.FormDataDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.query.FormDataQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormData;
|
||||||
|
import com.qqchen.deploy.backend.workflow.service.IFormDataService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据API控制器
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/forms/data")
|
||||||
|
@Tag(name = "表单数据管理", description = "表单数据管理相关接口")
|
||||||
|
public class FormDataApiController extends BaseController<FormData, FormDataDTO, Long, FormDataQuery> {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IFormDataService formDataService;
|
||||||
|
|
||||||
|
@Operation(summary = "提交表单数据")
|
||||||
|
@PostMapping("/submit")
|
||||||
|
public Response<FormDataDTO> submit(@RequestBody FormDataDTO dto) {
|
||||||
|
FormDataDTO result = formDataService.submit(dto);
|
||||||
|
return Response.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "根据业务标识查询表单数据")
|
||||||
|
@GetMapping("/business/{businessKey}")
|
||||||
|
public Response<FormDataDTO> getByBusinessKey(
|
||||||
|
@Parameter(description = "业务标识", required = true) @PathVariable String businessKey
|
||||||
|
) {
|
||||||
|
FormDataDTO result = formDataService.getByBusinessKey(businessKey);
|
||||||
|
return Response.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void exportData(jakarta.servlet.http.HttpServletResponse response, java.util.List<FormDataDTO> data) {
|
||||||
|
// 暂不支持导出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.api;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.api.Response;
|
||||||
|
import com.qqchen.deploy.backend.framework.controller.BaseController;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.FormDefinitionDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.query.FormDefinitionQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormDefinition;
|
||||||
|
import com.qqchen.deploy.backend.workflow.service.IFormDefinitionService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义API控制器
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/forms/definitions")
|
||||||
|
@Tag(name = "表单定义管理", description = "表单定义管理相关接口")
|
||||||
|
public class FormDefinitionApiController extends BaseController<FormDefinition, FormDefinitionDTO, Long, FormDefinitionQuery> {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IFormDefinitionService formDefinitionService;
|
||||||
|
|
||||||
|
@Operation(summary = "发布表单")
|
||||||
|
@PostMapping("/{id}/publish")
|
||||||
|
public Response<FormDefinitionDTO> publish(
|
||||||
|
@Parameter(description = "表单定义ID", required = true) @PathVariable Long id
|
||||||
|
) {
|
||||||
|
FormDefinitionDTO result = formDefinitionService.publish(id);
|
||||||
|
return Response.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "根据key获取最新版本表单")
|
||||||
|
@GetMapping("/latest/{key}")
|
||||||
|
public Response<FormDefinitionDTO> getLatestByKey(
|
||||||
|
@Parameter(description = "表单标识", required = true) @PathVariable String key
|
||||||
|
) {
|
||||||
|
FormDefinitionDTO result = formDefinitionService.getLatestByKey(key);
|
||||||
|
return Response.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void exportData(jakarta.servlet.http.HttpServletResponse response, java.util.List<FormDefinitionDTO> data) {
|
||||||
|
// 暂不支持导出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.api;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.controller.BaseController;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.WorkflowCategoryDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.query.WorkflowCategoryQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowCategory;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流分类API Controller
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/workflow/categories")
|
||||||
|
@Tag(name = "工作流分类管理", description = "工作流分类相关接口")
|
||||||
|
public class WorkflowCategoryApiController extends BaseController<WorkflowCategory, WorkflowCategoryDTO, Long, WorkflowCategoryQuery> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void exportData(HttpServletResponse response, List<WorkflowCategoryDTO> data) {
|
||||||
|
// TODO: 实现工作流分类数据导出逻辑
|
||||||
|
log.warn("工作流分类数据导出功能暂未实现");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.converter;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.FormCategoryDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormCategory;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类转换器
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Mapper(config = BaseConverter.class)
|
||||||
|
public interface FormCategoryConverter extends BaseConverter<FormCategory, FormCategoryDTO> {
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.converter;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.FormDataDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormData;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据转换器
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Mapper(config = BaseConverter.class)
|
||||||
|
public interface FormDataConverter extends BaseConverter<FormData, FormDataDTO> {
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.converter;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.FormDefinitionDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormDefinition;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义转换器
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Mapper(config = BaseConverter.class)
|
||||||
|
public interface FormDefinitionConverter extends BaseConverter<FormDefinition, FormDefinitionDTO> {
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.converter;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.WorkflowCategoryDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowCategory;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流分类转换器
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Mapper(config = BaseConverter.class)
|
||||||
|
public interface WorkflowCategoryConverter extends BaseConverter<WorkflowCategory, WorkflowCategoryDTO> {
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.dto;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类DTO
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Schema(description = "表单分类DTO")
|
||||||
|
public class FormCategoryDTO extends BaseDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类名称
|
||||||
|
*/
|
||||||
|
@Schema(description = "分类名称", example = "审批表单")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类编码
|
||||||
|
*/
|
||||||
|
@Schema(description = "分类编码", example = "APPROVAL")
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类描述
|
||||||
|
*/
|
||||||
|
@Schema(description = "分类描述", example = "用于审批流程的表单")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图标
|
||||||
|
*/
|
||||||
|
@Schema(description = "图标", example = "CheckCircleOutlined")
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序
|
||||||
|
*/
|
||||||
|
@Schema(description = "排序", example = "1")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
@Schema(description = "是否启用", example = "true")
|
||||||
|
private Boolean enabled;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.dto;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.FormBusinessTypeEnums;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.FormDataStatusEnums;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据DTO
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Schema(description = "表单数据DTO")
|
||||||
|
public class FormDataDTO extends BaseDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单定义ID", example = "1")
|
||||||
|
private Long formDefinitionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单标识(冗余存储,避免JOIN)
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单标识", example = "employee_info_form")
|
||||||
|
private String formKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单版本(冗余存储,用于历史追溯)
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单版本", example = "1")
|
||||||
|
private Integer formVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类ID(冗余存储,便于统计和查询)
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单分类ID", example = "1")
|
||||||
|
private Long categoryId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类信息(用于展示)
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单分类信息")
|
||||||
|
private FormCategoryDTO category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务标识(如工作流实例ID、订单号等)
|
||||||
|
*/
|
||||||
|
@Schema(description = "业务标识", example = "workflow_instance_123")
|
||||||
|
private String businessKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务类型(WORKFLOW-工作流、ORDER-订单、STANDALONE-独立表单)
|
||||||
|
*/
|
||||||
|
@Schema(description = "业务类型")
|
||||||
|
private FormBusinessTypeEnums businessType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单填写数据(用户提交的实际数据)
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单填写数据")
|
||||||
|
private Map<String, Object> data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单Schema快照(用于历史追溯,确保数据可还原,原样存储)
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单Schema快照")
|
||||||
|
private Map<String, Object> schemaSnapshot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交人
|
||||||
|
*/
|
||||||
|
@Schema(description = "提交人", example = "admin")
|
||||||
|
private String submitter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交时间
|
||||||
|
*/
|
||||||
|
@Schema(description = "提交时间", example = "2025-10-24T12:00:00")
|
||||||
|
private LocalDateTime submitTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态(DRAFT-草稿、SUBMITTED-已提交、COMPLETED-已完成、REJECTED-已拒绝)
|
||||||
|
*/
|
||||||
|
@Schema(description = "状态")
|
||||||
|
private FormDataStatusEnums status;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.dto;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.FormDefinitionStatusEnums;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义DTO
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Schema(description = "表单定义DTO")
|
||||||
|
public class FormDefinitionDTO extends BaseDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单名称
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单名称", example = "员工信息登记表")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单标识(业务唯一)
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单标识", example = "employee_info_form")
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单版本号
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单版本号", example = "1")
|
||||||
|
private Integer formVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类ID
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单分类ID", example = "1")
|
||||||
|
private Long categoryId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类信息(用于展示)
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单分类信息")
|
||||||
|
private FormCategoryDTO category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单描述
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单描述", example = "用于采集员工基本信息")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单Schema(前端设计器导出的JSON结构,原样存储)
|
||||||
|
*/
|
||||||
|
@Schema(description = "表单Schema")
|
||||||
|
private Map<String, Object> schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签(用于分类和搜索)
|
||||||
|
*/
|
||||||
|
@Schema(description = "标签列表", example = "[\"员工管理\", \"信息采集\"]")
|
||||||
|
private List<String> tags;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态(DRAFT-草稿、PUBLISHED-已发布、DISABLED-已禁用)
|
||||||
|
*/
|
||||||
|
@Schema(description = "状态")
|
||||||
|
private FormDefinitionStatusEnums status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为模板
|
||||||
|
*/
|
||||||
|
@Schema(description = "是否为模板", example = "false")
|
||||||
|
private Boolean isTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,19 +1,64 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.dto;
|
package com.qqchen.deploy.backend.workflow.dto;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流分类DTO
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class WorkflowCategoryDTO {
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Schema(description = "工作流分类DTO")
|
||||||
|
public class WorkflowCategoryDTO extends BaseDTO {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类名称
|
||||||
|
*/
|
||||||
|
@Schema(description = "分类名称", example = "脚本执行")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类编码(唯一)
|
||||||
|
*/
|
||||||
|
@Schema(description = "分类编码", example = "SCRIPT_EXECUTION")
|
||||||
private String code;
|
private String code;
|
||||||
|
|
||||||
private String lable;
|
/**
|
||||||
|
* 分类描述
|
||||||
|
*/
|
||||||
|
@Schema(description = "分类描述", example = "用于执行各类脚本的流程")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
private List<WorkflowTriggerTypeDTO> supportedTriggers;
|
/**
|
||||||
|
* 图标
|
||||||
|
*/
|
||||||
|
@Schema(description = "图标", example = "CodeOutlined")
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序
|
||||||
|
*/
|
||||||
|
@Schema(description = "排序", example = "1")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的触发方式列表
|
||||||
|
*/
|
||||||
|
@Schema(description = "支持的触发方式列表", example = "[\"MANUAL\",\"SCHEDULED\"]")
|
||||||
|
private List<String> supportedTriggers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
@Schema(description = "是否启用", example = "true")
|
||||||
|
private Boolean enabled;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,12 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.dto;
|
package com.qqchen.deploy.backend.workflow.dto;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
|
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.FormDefinitionDTO;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraph;
|
import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraph;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowCategoryEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowDefinitionStatusEnums;
|
import com.qqchen.deploy.backend.workflow.enums.WorkflowDefinitionStatusEnums;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowTriggerTypeEnum;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流定义DTO
|
* 工作流定义DTO
|
||||||
*
|
*
|
||||||
@ -32,14 +28,24 @@ public class WorkflowDefinitionDTO extends BaseDTO {
|
|||||||
private String key;
|
private String key;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分类
|
* 分类ID
|
||||||
*/
|
*/
|
||||||
private WorkflowCategoryEnum category;
|
private Long categoryId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 触发类型列表
|
* 分类信息(用于展示)
|
||||||
*/
|
*/
|
||||||
private List<WorkflowTriggerTypeEnum> triggers;
|
private WorkflowCategoryDTO category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动表单ID
|
||||||
|
*/
|
||||||
|
private Long formDefinitionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动表单信息(用于展示)
|
||||||
|
*/
|
||||||
|
private FormDefinitionDTO formDefinition;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流程版本
|
* 流程版本
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.dto.query;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.annotation.QueryField;
|
||||||
|
import com.qqchen.deploy.backend.framework.enums.QueryType;
|
||||||
|
import com.qqchen.deploy.backend.framework.query.BaseQuery;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类查询对象
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class FormCategoryQuery extends BaseQuery {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类名称(模糊查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "name", type = QueryType.LIKE)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类编码(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "code", type = QueryType.EQUAL)
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "enabled", type = QueryType.EQUAL)
|
||||||
|
private Boolean enabled;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.dto.query;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.annotation.QueryField;
|
||||||
|
import com.qqchen.deploy.backend.framework.enums.QueryType;
|
||||||
|
import com.qqchen.deploy.backend.framework.query.BaseQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.FormBusinessTypeEnums;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.FormDataStatusEnums;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据查询对象
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class FormDataQuery extends BaseQuery {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义ID(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "formDefinitionId", type = QueryType.EQUAL)
|
||||||
|
private Long formDefinitionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单标识(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "formKey", type = QueryType.EQUAL)
|
||||||
|
private String formKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类ID(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "categoryId", type = QueryType.EQUAL)
|
||||||
|
private Long categoryId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务标识(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "businessKey", type = QueryType.EQUAL)
|
||||||
|
private String businessKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务类型(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "businessType", type = QueryType.EQUAL)
|
||||||
|
private FormBusinessTypeEnums businessType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交人(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "submitter", type = QueryType.EQUAL)
|
||||||
|
private String submitter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "status", type = QueryType.EQUAL)
|
||||||
|
private FormDataStatusEnums status;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.dto.query;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.annotation.QueryField;
|
||||||
|
import com.qqchen.deploy.backend.framework.enums.QueryType;
|
||||||
|
import com.qqchen.deploy.backend.framework.query.BaseQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.FormDefinitionStatusEnums;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义查询对象
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class FormDefinitionQuery extends BaseQuery {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单名称(模糊查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "name", type = QueryType.LIKE)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单标识(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "key", type = QueryType.EQUAL)
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单版本(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "formVersion", type = QueryType.EQUAL)
|
||||||
|
private Integer formVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类ID(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "categoryId", type = QueryType.EQUAL)
|
||||||
|
private Long categoryId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "status", type = QueryType.EQUAL)
|
||||||
|
private FormDefinitionStatusEnums status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为模板(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "isTemplate", type = QueryType.EQUAL)
|
||||||
|
private Boolean isTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.dto.query;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.annotation.QueryField;
|
||||||
|
import com.qqchen.deploy.backend.framework.enums.QueryType;
|
||||||
|
import com.qqchen.deploy.backend.framework.query.BaseQuery;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流分类查询对象
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class WorkflowCategoryQuery extends BaseQuery {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类名称(模糊查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "name", type = QueryType.LIKE)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类编码(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "code", type = QueryType.EQUAL)
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用(精确查询)
|
||||||
|
*/
|
||||||
|
@QueryField(field = "enabled", type = QueryType.EQUAL)
|
||||||
|
private Boolean enabled;
|
||||||
|
}
|
||||||
|
|
||||||
@ -3,6 +3,7 @@ package com.qqchen.deploy.backend.workflow.dto.query;
|
|||||||
import com.qqchen.deploy.backend.framework.annotation.QueryField;
|
import com.qqchen.deploy.backend.framework.annotation.QueryField;
|
||||||
import com.qqchen.deploy.backend.framework.enums.QueryType;
|
import com.qqchen.deploy.backend.framework.enums.QueryType;
|
||||||
import com.qqchen.deploy.backend.framework.query.BaseQuery;
|
import com.qqchen.deploy.backend.framework.query.BaseQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.WorkflowDefinitionStatusEnums;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -25,9 +26,21 @@ public class WorkflowDefinitionQuery extends BaseQuery {
|
|||||||
@QueryField(field = "key", type = QueryType.EQUAL)
|
@QueryField(field = "key", type = QueryType.EQUAL)
|
||||||
private String key;
|
private String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类ID
|
||||||
|
*/
|
||||||
|
@QueryField(field = "categoryId", type = QueryType.EQUAL)
|
||||||
|
private Long categoryId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流程版本
|
* 流程版本
|
||||||
*/
|
*/
|
||||||
@QueryField(field = "version", type = QueryType.EQUAL)
|
@QueryField(field = "flowVersion", type = QueryType.EQUAL)
|
||||||
private Integer version;
|
private Integer flowVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程状态
|
||||||
|
*/
|
||||||
|
@QueryField(field = "status", type = QueryType.EQUAL)
|
||||||
|
private WorkflowDefinitionStatusEnums status;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.entity;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
|
||||||
|
import com.qqchen.deploy.backend.framework.domain.Entity;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类实体
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Table(name = "form_category")
|
||||||
|
@jakarta.persistence.Entity
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@LogicDelete
|
||||||
|
public class FormCategory extends Entity<Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类名称
|
||||||
|
*/
|
||||||
|
@Column(nullable = false, length = 100)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类编码(唯一)
|
||||||
|
*/
|
||||||
|
@Column(nullable = false, length = 50, unique = true)
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类描述
|
||||||
|
*/
|
||||||
|
@Column(length = 500)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图标
|
||||||
|
*/
|
||||||
|
@Column(length = 50)
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序
|
||||||
|
*/
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Boolean enabled;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.entity;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
|
||||||
|
import com.qqchen.deploy.backend.framework.domain.Entity;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.FormBusinessTypeEnums;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.FormDataStatusEnums;
|
||||||
|
import com.vladmihalcea.hibernate.type.json.JsonType;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.hibernate.annotations.Type;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据实体
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Table(name = "form_data")
|
||||||
|
@jakarta.persistence.Entity
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@LogicDelete
|
||||||
|
public class FormData extends Entity<Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义ID
|
||||||
|
*/
|
||||||
|
@Column(name = "form_definition_id", nullable = false)
|
||||||
|
private Long formDefinitionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单标识(冗余存储,避免JOIN)
|
||||||
|
*/
|
||||||
|
@Column(name = "form_key", nullable = false)
|
||||||
|
private String formKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单版本(冗余存储,用于历史追溯)
|
||||||
|
*/
|
||||||
|
@Column(name = "form_version", nullable = false)
|
||||||
|
private Integer formVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类ID(冗余存储,便于统计和查询)
|
||||||
|
*/
|
||||||
|
@Column(name = "category_id")
|
||||||
|
private Long categoryId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务标识(如工作流实例ID、订单号等)
|
||||||
|
*/
|
||||||
|
@Column(name = "business_key")
|
||||||
|
private String businessKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务类型(WORKFLOW-工作流、ORDER-订单、STANDALONE-独立表单)
|
||||||
|
*/
|
||||||
|
@Column(name = "business_type")
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private FormBusinessTypeEnums businessType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单填写数据(用户提交的实际数据)
|
||||||
|
*/
|
||||||
|
@Type(JsonType.class)
|
||||||
|
@Column(name = "data", nullable = false, columnDefinition = "json")
|
||||||
|
private Map<String, Object> data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单Schema快照(用于历史追溯,确保数据可还原,原样存储)
|
||||||
|
*/
|
||||||
|
@Type(JsonType.class)
|
||||||
|
@Column(name = "schema_snapshot", nullable = false, columnDefinition = "json")
|
||||||
|
private Map<String, Object> schemaSnapshot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交人
|
||||||
|
*/
|
||||||
|
@Column(name = "submitter")
|
||||||
|
private String submitter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交时间
|
||||||
|
*/
|
||||||
|
@Column(name = "submit_time")
|
||||||
|
private LocalDateTime submitTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态(DRAFT-草稿、SUBMITTED-已提交、COMPLETED-已完成、REJECTED-已拒绝)
|
||||||
|
*/
|
||||||
|
@Column(name = "status", nullable = false)
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private FormDataStatusEnums status;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.entity;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
|
||||||
|
import com.qqchen.deploy.backend.framework.domain.Entity;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.FormDefinitionStatusEnums;
|
||||||
|
import com.vladmihalcea.hibernate.type.json.JsonType;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.hibernate.annotations.Type;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义实体
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Table(name = "form_definition")
|
||||||
|
@jakarta.persistence.Entity
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@LogicDelete
|
||||||
|
public class FormDefinition extends Entity<Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单名称
|
||||||
|
*/
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单标识(业务唯一)
|
||||||
|
*/
|
||||||
|
@Column(name = "`key`", nullable = false)
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单版本号
|
||||||
|
*/
|
||||||
|
@Column(name = "form_version", nullable = false)
|
||||||
|
private Integer formVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类ID
|
||||||
|
*/
|
||||||
|
@Column(name = "category_id")
|
||||||
|
private Long categoryId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单描述
|
||||||
|
*/
|
||||||
|
@Column(name = "description", columnDefinition = "TEXT")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单Schema(前端设计器导出的JSON结构,原样存储)
|
||||||
|
*/
|
||||||
|
@Type(JsonType.class)
|
||||||
|
@Column(name = "`schema`", nullable = false, columnDefinition = "json")
|
||||||
|
private Map<String, Object> schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签(用于分类和搜索)
|
||||||
|
*/
|
||||||
|
@Type(JsonType.class)
|
||||||
|
@Column(name = "tags", columnDefinition = "json")
|
||||||
|
private List<String> tags;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态(DRAFT-草稿、PUBLISHED-已发布、DISABLED-已禁用)
|
||||||
|
*/
|
||||||
|
@Column(name = "status", nullable = false)
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private FormDefinitionStatusEnums status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为模板
|
||||||
|
*/
|
||||||
|
@Column(name = "is_template", nullable = false)
|
||||||
|
private Boolean isTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.entity;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
|
||||||
|
import com.qqchen.deploy.backend.framework.domain.Entity;
|
||||||
|
import com.vladmihalcea.hibernate.type.json.JsonType;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.hibernate.annotations.Type;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流分类实体
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Table(name = "workflow_category")
|
||||||
|
@jakarta.persistence.Entity
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@LogicDelete
|
||||||
|
public class WorkflowCategory extends Entity<Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类名称
|
||||||
|
*/
|
||||||
|
@Column(name = "name", nullable = false, length = 100)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类编码(唯一,如SCRIPT_EXECUTION)
|
||||||
|
*/
|
||||||
|
@Column(name = "code", nullable = false, length = 50, unique = true)
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类描述
|
||||||
|
*/
|
||||||
|
@Column(name = "description", length = 500)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图标
|
||||||
|
*/
|
||||||
|
@Column(name = "icon", length = 50)
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序
|
||||||
|
*/
|
||||||
|
@Column(name = "sort", nullable = false)
|
||||||
|
private Integer sort = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的触发方式列表(JSON数组,如["MANUAL","SCHEDULED"])
|
||||||
|
*/
|
||||||
|
@Type(JsonType.class)
|
||||||
|
@Column(name = "supported_triggers", columnDefinition = "json")
|
||||||
|
private List<String> supportedTriggers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
@Column(name = "enabled", nullable = false)
|
||||||
|
private Boolean enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,16 +1,11 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.entity;
|
package com.qqchen.deploy.backend.workflow.entity;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
|
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraph;
|
import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraph;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowCategoryEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowDefinitionStatusEnums;
|
import com.qqchen.deploy.backend.workflow.enums.WorkflowDefinitionStatusEnums;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowTriggerTypeEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.converter.WorkflowGraphType;
|
import com.qqchen.deploy.backend.workflow.entity.converter.WorkflowGraphType;
|
||||||
import com.qqchen.deploy.backend.framework.domain.Entity;
|
import com.qqchen.deploy.backend.framework.domain.Entity;
|
||||||
import com.vladmihalcea.hibernate.type.json.JsonType;
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Convert;
|
|
||||||
import jakarta.persistence.EnumType;
|
import jakarta.persistence.EnumType;
|
||||||
import jakarta.persistence.Enumerated;
|
import jakarta.persistence.Enumerated;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
@ -18,8 +13,6 @@ import lombok.Data;
|
|||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import org.hibernate.annotations.Type;
|
import org.hibernate.annotations.Type;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流定义实体
|
* 工作流定义实体
|
||||||
*/
|
*/
|
||||||
@ -43,18 +36,18 @@ public class WorkflowDefinition extends Entity<Long> {
|
|||||||
private String key;
|
private String key;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流程分类
|
* 流程分类ID(外键关联workflow_category)
|
||||||
*/
|
*/
|
||||||
@Column(name = "category", nullable = false)
|
@Column(name = "category_id")
|
||||||
@Enumerated(EnumType.STRING)
|
private Long categoryId;
|
||||||
private WorkflowCategoryEnum category;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动表单ID(外键关联form_definition)
|
||||||
|
*/
|
||||||
|
@Column(name = "form_definition_id")
|
||||||
|
private Long formDefinitionId;
|
||||||
|
|
||||||
@Column(name = "triggers", nullable = false)
|
@Column(name = "process_definition_id")
|
||||||
@Convert(converter = WorkflowTriggerTypeListConverter.class)
|
|
||||||
private List<WorkflowTriggerTypeEnum> triggers;
|
|
||||||
|
|
||||||
@Column(name = "process_definition_id", nullable = false)
|
|
||||||
private String processDefinitionId;
|
private String processDefinitionId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,12 +82,4 @@ public class WorkflowDefinition extends Entity<Long> {
|
|||||||
*/
|
*/
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 流程标签,用于分组和过滤
|
|
||||||
*/
|
|
||||||
@Type(JsonType.class)
|
|
||||||
@Column(name = "tags", columnDefinition = "json")
|
|
||||||
private JsonNode tags;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单业务类型枚举
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum FormBusinessTypeEnums {
|
||||||
|
|
||||||
|
WORKFLOW("WORKFLOW", "工作流"),
|
||||||
|
ORDER("ORDER", "订单"),
|
||||||
|
STANDALONE("STANDALONE", "独立表单");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
FormBusinessTypeEnums(String code, String description) {
|
||||||
|
this.code = code;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据状态枚举
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum FormDataStatusEnums {
|
||||||
|
|
||||||
|
DRAFT("DRAFT", "草稿"),
|
||||||
|
SUBMITTED("SUBMITTED", "已提交"),
|
||||||
|
COMPLETED("COMPLETED", "已完成"),
|
||||||
|
REJECTED("REJECTED", "已拒绝");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
FormDataStatusEnums(String code, String description) {
|
||||||
|
this.code = code;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义状态枚举
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum FormDefinitionStatusEnums {
|
||||||
|
|
||||||
|
DRAFT("DRAFT", "草稿"),
|
||||||
|
PUBLISHED("PUBLISHED", "已发布"),
|
||||||
|
DISABLED("DISABLED", "已禁用");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
FormDefinitionStatusEnums(String code, String description) {
|
||||||
|
this.code = code;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.enums;
|
|
||||||
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作流节点分类枚举
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
public enum WorkflowCategoryEnum {
|
|
||||||
// 1. 脚本执行类
|
|
||||||
SCRIPT_EXECUTION(
|
|
||||||
"脚本执行",
|
|
||||||
"用于执行各类脚本的流程",
|
|
||||||
Arrays.asList(WorkflowTriggerTypeEnum.MANUAL, WorkflowTriggerTypeEnum.SCHEDULED)
|
|
||||||
),
|
|
||||||
|
|
||||||
// 2. 部署类
|
|
||||||
DEPLOYMENT(
|
|
||||||
"应用部署",
|
|
||||||
"用于应用部署的流程",
|
|
||||||
Arrays.asList(WorkflowTriggerTypeEnum.MANUAL, WorkflowTriggerTypeEnum.SCHEDULED, WorkflowTriggerTypeEnum.APPROVAL)
|
|
||||||
),
|
|
||||||
|
|
||||||
// 3. 数据同步类
|
|
||||||
DATA_SYNC(
|
|
||||||
"数据同步",
|
|
||||||
"用于第三方系统数据同步的流程",
|
|
||||||
Arrays.asList(WorkflowTriggerTypeEnum.MANUAL, WorkflowTriggerTypeEnum.SCHEDULED)
|
|
||||||
),
|
|
||||||
|
|
||||||
// 4. 配置同步类
|
|
||||||
CONFIG_SYNC(
|
|
||||||
"配置同步",
|
|
||||||
"用于配置中心数据同步的流程",
|
|
||||||
Arrays.asList(WorkflowTriggerTypeEnum.MANUAL, WorkflowTriggerTypeEnum.APPROVAL)
|
|
||||||
),
|
|
||||||
|
|
||||||
// 5. 审批流程类
|
|
||||||
APPROVAL(
|
|
||||||
"审批流程",
|
|
||||||
"纯审批流程",
|
|
||||||
Arrays.asList(WorkflowTriggerTypeEnum.MANUAL)
|
|
||||||
),
|
|
||||||
|
|
||||||
// 6. 其他类型(用于扩展)
|
|
||||||
OTHER(
|
|
||||||
"其他",
|
|
||||||
"其他类型流程",
|
|
||||||
Arrays.asList(WorkflowTriggerTypeEnum.MANUAL)
|
|
||||||
);
|
|
||||||
|
|
||||||
private final String label; // 显示名称
|
|
||||||
|
|
||||||
private final String description; // 描述
|
|
||||||
|
|
||||||
private final List<WorkflowTriggerTypeEnum> supportedTriggers; // 支持的触发方式
|
|
||||||
|
|
||||||
WorkflowCategoryEnum(String label, String description, List<WorkflowTriggerTypeEnum> supportedTriggers) {
|
|
||||||
this.label = label;
|
|
||||||
this.description = description;
|
|
||||||
this.supportedTriggers = supportedTriggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否支持某种触发方式
|
|
||||||
public boolean supportsTriggerType(WorkflowTriggerTypeEnum triggerType) {
|
|
||||||
return supportedTriggers.contains(triggerType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.repository;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormCategory;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类Repository接口
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface IFormCategoryRepository extends IBaseRepository<FormCategory, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码查询
|
||||||
|
*
|
||||||
|
* @param code 分类编码
|
||||||
|
* @return 表单分类
|
||||||
|
*/
|
||||||
|
Optional<FormCategory> findByCodeAndDeletedFalse(String code);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查编码是否存在
|
||||||
|
*
|
||||||
|
* @param code 分类编码
|
||||||
|
* @return 是否存在
|
||||||
|
*/
|
||||||
|
boolean existsByCodeAndDeletedFalse(String code);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有启用的分类(按排序)
|
||||||
|
*
|
||||||
|
* @return 分类列表
|
||||||
|
*/
|
||||||
|
List<FormCategory> findByEnabledTrueAndDeletedFalseOrderBySortAsc();
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.repository;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormData;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.FormDataStatusEnums;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据Repository接口
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface IFormDataRepository extends IBaseRepository<FormData, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据业务标识查询
|
||||||
|
*
|
||||||
|
* @param businessKey 业务标识
|
||||||
|
* @return 表单数据
|
||||||
|
*/
|
||||||
|
Optional<FormData> findByBusinessKeyAndDeletedFalse(String businessKey);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据表单标识分页查询
|
||||||
|
*
|
||||||
|
* @param formKey 表单标识
|
||||||
|
* @param pageable 分页参数
|
||||||
|
* @return 分页结果
|
||||||
|
*/
|
||||||
|
Page<FormData> findByFormKeyAndDeletedFalse(String formKey, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据提交人分页查询
|
||||||
|
*
|
||||||
|
* @param submitter 提交人
|
||||||
|
* @param pageable 分页参数
|
||||||
|
* @return 分页结果
|
||||||
|
*/
|
||||||
|
Page<FormData> findBySubmitterAndDeletedFalse(String submitter, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据状态分页查询
|
||||||
|
*
|
||||||
|
* @param status 状态
|
||||||
|
* @param pageable 分页参数
|
||||||
|
* @return 分页结果
|
||||||
|
*/
|
||||||
|
Page<FormData> findByStatusAndDeletedFalse(FormDataStatusEnums status, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据表单定义ID查询列表
|
||||||
|
*
|
||||||
|
* @param formDefinitionId 表单定义ID
|
||||||
|
* @return 表单数据列表
|
||||||
|
*/
|
||||||
|
List<FormData> findByFormDefinitionIdAndDeletedFalse(Long formDefinitionId);
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.repository;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormDefinition;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.FormDefinitionStatusEnums;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义Repository接口
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface IFormDefinitionRepository extends IBaseRepository<FormDefinition, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据表单标识和版本查询
|
||||||
|
*
|
||||||
|
* @param key 表单标识
|
||||||
|
* @param formVersion 表单版本
|
||||||
|
* @return 表单定义
|
||||||
|
*/
|
||||||
|
Optional<FormDefinition> findByKeyAndFormVersionAndDeletedFalse(String key, Integer formVersion);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据表单标识查询最新版本
|
||||||
|
*
|
||||||
|
* @param key 表单标识
|
||||||
|
* @return 表单定义
|
||||||
|
*/
|
||||||
|
Optional<FormDefinition> findFirstByKeyAndDeletedFalseOrderByFormVersionDesc(String key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查表单标识是否存在
|
||||||
|
*
|
||||||
|
* @param key 表单标识
|
||||||
|
* @return 是否存在
|
||||||
|
*/
|
||||||
|
boolean existsByKeyAndDeletedFalse(String key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据状态分页查询
|
||||||
|
*
|
||||||
|
* @param status 状态
|
||||||
|
* @param pageable 分页参数
|
||||||
|
* @return 分页结果
|
||||||
|
*/
|
||||||
|
Page<FormDefinition> findByStatusAndDeletedFalse(FormDefinitionStatusEnums status, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据状态查询列表
|
||||||
|
*
|
||||||
|
* @param status 状态
|
||||||
|
* @return 表单定义列表
|
||||||
|
*/
|
||||||
|
List<FormDefinition> findByStatusAndDeletedFalse(FormDefinitionStatusEnums status);
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.repository;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowCategory;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流分类Repository
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface IWorkflowCategoryRepository extends IBaseRepository<WorkflowCategory, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码查询分类
|
||||||
|
*
|
||||||
|
* @param code 分类编码
|
||||||
|
* @return 工作流分类
|
||||||
|
*/
|
||||||
|
WorkflowCategory findByCodeAndDeletedFalse(String code);
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.service;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.service.IBaseService;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.FormCategoryDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.query.FormCategoryQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormCategory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类服务接口
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
public interface IFormCategoryService extends IBaseService<FormCategory, FormCategoryDTO, FormCategoryQuery, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有启用的分类
|
||||||
|
*
|
||||||
|
* @return 分类列表
|
||||||
|
*/
|
||||||
|
List<FormCategoryDTO> findAllEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.service;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.service.IBaseService;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.FormDataDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.query.FormDataQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据服务接口
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
public interface IFormDataService extends IBaseService<FormData, FormDataDTO, FormDataQuery, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交表单数据
|
||||||
|
*
|
||||||
|
* @param dto 表单数据DTO
|
||||||
|
* @return 提交后的表单数据
|
||||||
|
*/
|
||||||
|
FormDataDTO submit(FormDataDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据业务标识查询表单数据
|
||||||
|
*
|
||||||
|
* @param businessKey 业务标识
|
||||||
|
* @return 表单数据
|
||||||
|
*/
|
||||||
|
FormDataDTO getByBusinessKey(String businessKey);
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.service;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.service.IBaseService;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.FormDefinitionDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.query.FormDefinitionQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义服务接口
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
public interface IFormDefinitionService extends IBaseService<FormDefinition, FormDefinitionDTO, FormDefinitionQuery, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布表单(版本不可变)
|
||||||
|
* 如果当前表单已发布,则创建新版本;否则直接发布
|
||||||
|
*
|
||||||
|
* @param id 表单定义ID
|
||||||
|
* @return 发布后的表单定义
|
||||||
|
*/
|
||||||
|
FormDefinitionDTO publish(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据表单标识获取最新版本
|
||||||
|
*
|
||||||
|
* @param key 表单标识
|
||||||
|
* @return 表单定义
|
||||||
|
*/
|
||||||
|
FormDefinitionDTO getLatestByKey(String key);
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.service;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.service.IBaseService;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.WorkflowCategoryDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.query.WorkflowCategoryQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowCategory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流分类Service接口
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
public interface IWorkflowCategoryService extends IBaseService<WorkflowCategory, WorkflowCategoryDTO, WorkflowCategoryQuery, Long> {
|
||||||
|
}
|
||||||
|
|
||||||
@ -9,7 +9,6 @@ import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceCreateDTO;
|
|||||||
import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO;
|
import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.query.WorkflowDefinitionQuery;
|
import com.qqchen.deploy.backend.workflow.dto.query.WorkflowDefinitionQuery;
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowCategoryEnum;
|
|
||||||
import org.flowable.engine.repository.Deployment;
|
import org.flowable.engine.repository.Deployment;
|
||||||
import org.flowable.engine.runtime.ProcessInstance;
|
import org.flowable.engine.runtime.ProcessInstance;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.service.impl;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.FormCategoryDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.query.FormCategoryQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormCategory;
|
||||||
|
import com.qqchen.deploy.backend.workflow.repository.IFormCategoryRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.service.IFormCategoryService;
|
||||||
|
import com.qqchen.deploy.backend.workflow.converter.FormCategoryConverter;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类服务实现
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class FormCategoryServiceImpl extends BaseServiceImpl<FormCategory, FormCategoryDTO, FormCategoryQuery, Long> implements IFormCategoryService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IFormCategoryRepository formCategoryRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private FormCategoryConverter formCategoryConverter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<FormCategoryDTO> findAllEnabled() {
|
||||||
|
log.info("查询所有启用的表单分类");
|
||||||
|
List<FormCategory> categories = formCategoryRepository.findByEnabledTrueAndDeletedFalseOrderBySortAsc();
|
||||||
|
return categories.stream()
|
||||||
|
.map(formCategoryConverter::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.service.impl;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
||||||
|
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.FormDataDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.query.FormDataQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormData;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormDefinition;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.FormDataStatusEnums;
|
||||||
|
import com.qqchen.deploy.backend.workflow.repository.IFormDataRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.repository.IFormDefinitionRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.service.IFormDataService;
|
||||||
|
import com.qqchen.deploy.backend.workflow.converter.FormDataConverter;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据服务实现
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class FormDataServiceImpl extends BaseServiceImpl<FormData, FormDataDTO, FormDataQuery, Long> implements IFormDataService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IFormDataRepository formDataRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IFormDefinitionRepository formDefinitionRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private FormDataConverter formDataConverter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public FormDataDTO submit(FormDataDTO dto) {
|
||||||
|
log.info("提交表单数据: formKey={}, submitter={}", dto.getFormKey(), dto.getSubmitter());
|
||||||
|
|
||||||
|
// 1. 查询表单定义,获取schema快照
|
||||||
|
FormDefinition formDefinition = formDefinitionRepository.findById(dto.getFormDefinitionId())
|
||||||
|
.orElseThrow(() -> new BusinessException(com.qqchen.deploy.backend.framework.enums.ResponseCode.FORM_DEFINITION_NOT_FOUND));
|
||||||
|
|
||||||
|
// 2. 创建表单数据
|
||||||
|
FormData formData = formDataConverter.toEntity(dto);
|
||||||
|
formData.setFormKey(formDefinition.getKey());
|
||||||
|
formData.setFormVersion(formDefinition.getFormVersion());
|
||||||
|
formData.setCategoryId(formDefinition.getCategoryId()); // 冗余存储分类ID
|
||||||
|
formData.setSchemaSnapshot(formDefinition.getSchema()); // 保存schema快照
|
||||||
|
formData.setSubmitTime(LocalDateTime.now());
|
||||||
|
formData.setStatus(FormDataStatusEnums.SUBMITTED);
|
||||||
|
|
||||||
|
// 3. 保存
|
||||||
|
FormData savedFormData = formDataRepository.save(formData);
|
||||||
|
log.info("表单数据提交成功: id={}, formKey={}", savedFormData.getId(), savedFormData.getFormKey());
|
||||||
|
|
||||||
|
return formDataConverter.toDto(savedFormData);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FormDataDTO getByBusinessKey(String businessKey) {
|
||||||
|
log.info("根据业务标识查询表单数据: businessKey={}", businessKey);
|
||||||
|
|
||||||
|
FormData formData = formDataRepository.findByBusinessKeyAndDeletedFalse(businessKey)
|
||||||
|
.orElseThrow(() -> new BusinessException(com.qqchen.deploy.backend.framework.enums.ResponseCode.FORM_DATA_NOT_FOUND));
|
||||||
|
|
||||||
|
return formDataConverter.toDto(formData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,133 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.service.impl;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
||||||
|
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.FormDefinitionDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.query.FormDefinitionQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.FormDefinition;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.FormDefinitionStatusEnums;
|
||||||
|
import com.qqchen.deploy.backend.workflow.repository.IFormDefinitionRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.service.IFormDefinitionService;
|
||||||
|
import com.qqchen.deploy.backend.workflow.converter.FormDefinitionConverter;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义服务实现
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class FormDefinitionServiceImpl extends BaseServiceImpl<FormDefinition, FormDefinitionDTO, FormDefinitionQuery, Long> implements IFormDefinitionService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IFormDefinitionRepository formDefinitionRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private FormDefinitionConverter formDefinitionConverter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public FormDefinitionDTO create(FormDefinitionDTO dto) {
|
||||||
|
log.info("创建表单定义: name={}, key={}", dto.getName(), dto.getKey());
|
||||||
|
|
||||||
|
// 1. 转换为实体
|
||||||
|
FormDefinition formDefinition = formDefinitionConverter.toEntity(dto);
|
||||||
|
|
||||||
|
// 2. 后端控制版本号和状态(前端传递的值会被覆盖)
|
||||||
|
formDefinition.setFormVersion(1); // 新建表单,版本号固定为 1
|
||||||
|
formDefinition.setStatus(FormDefinitionStatusEnums.DRAFT); // 新建表单,状态固定为草稿
|
||||||
|
|
||||||
|
// 3. 保存
|
||||||
|
FormDefinition savedForm = formDefinitionRepository.save(formDefinition);
|
||||||
|
log.info("表单定义创建成功: id={}, key={}, version={}", savedForm.getId(), savedForm.getKey(), savedForm.getFormVersion());
|
||||||
|
|
||||||
|
return formDefinitionConverter.toDto(savedForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public FormDefinitionDTO update(Long id, FormDefinitionDTO dto) {
|
||||||
|
log.info("更新表单定义: id={}, name={}", id, dto.getName());
|
||||||
|
|
||||||
|
// 1. 查询原始表单定义
|
||||||
|
FormDefinition existingForm = formDefinitionRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new BusinessException(com.qqchen.deploy.backend.framework.enums.ResponseCode.FORM_DEFINITION_NOT_FOUND));
|
||||||
|
|
||||||
|
// 2. 保留后端控制的字段(版本号和状态由后端控制,不允许前端修改)
|
||||||
|
Integer originalFormVersion = existingForm.getFormVersion();
|
||||||
|
FormDefinitionStatusEnums originalStatus = existingForm.getStatus();
|
||||||
|
|
||||||
|
// 3. 转换并更新允许修改的字段
|
||||||
|
FormDefinition formDefinition = formDefinitionConverter.toEntity(dto);
|
||||||
|
formDefinition.setId(id); // 使用路径参数的ID
|
||||||
|
formDefinition.setFormVersion(originalFormVersion); // 保留原始版本号
|
||||||
|
formDefinition.setStatus(originalStatus); // 保留原始状态
|
||||||
|
|
||||||
|
// 4. 保存
|
||||||
|
FormDefinition savedForm = formDefinitionRepository.save(formDefinition);
|
||||||
|
log.info("表单定义更新成功: id={}, version={}, status={}", savedForm.getId(), savedForm.getFormVersion(), savedForm.getStatus());
|
||||||
|
|
||||||
|
return formDefinitionConverter.toDto(savedForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public FormDefinitionDTO publish(Long id) {
|
||||||
|
log.info("发布表单定义,ID: {}", id);
|
||||||
|
|
||||||
|
// 1. 查询当前表单定义
|
||||||
|
FormDefinition currentForm = formDefinitionRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new BusinessException(com.qqchen.deploy.backend.framework.enums.ResponseCode.FORM_DEFINITION_NOT_FOUND));
|
||||||
|
|
||||||
|
// 2. 检查是否已发布
|
||||||
|
if (FormDefinitionStatusEnums.PUBLISHED.equals(currentForm.getStatus())) {
|
||||||
|
log.info("表单已发布,创建新版本: key={}, currentVersion={}", currentForm.getKey(), currentForm.getFormVersion());
|
||||||
|
|
||||||
|
// 创建新版本
|
||||||
|
FormDefinition newVersion = new FormDefinition();
|
||||||
|
newVersion.setName(currentForm.getName());
|
||||||
|
newVersion.setKey(currentForm.getKey());
|
||||||
|
newVersion.setFormVersion(currentForm.getFormVersion() + 1);
|
||||||
|
newVersion.setCategoryId(currentForm.getCategoryId());
|
||||||
|
newVersion.setDescription(currentForm.getDescription());
|
||||||
|
newVersion.setSchema(currentForm.getSchema());
|
||||||
|
newVersion.setTags(currentForm.getTags());
|
||||||
|
newVersion.setStatus(FormDefinitionStatusEnums.PUBLISHED);
|
||||||
|
newVersion.setIsTemplate(currentForm.getIsTemplate());
|
||||||
|
|
||||||
|
FormDefinition savedNewVersion = formDefinitionRepository.save(newVersion);
|
||||||
|
log.info("新版本创建成功: id={}, version={}", savedNewVersion.getId(), savedNewVersion.getFormVersion());
|
||||||
|
|
||||||
|
return formDefinitionConverter.toDto(savedNewVersion);
|
||||||
|
} else {
|
||||||
|
// 3. 首次发布,直接更新状态
|
||||||
|
log.info("首次发布表单: key={}, version={}", currentForm.getKey(), currentForm.getFormVersion());
|
||||||
|
currentForm.setStatus(FormDefinitionStatusEnums.PUBLISHED);
|
||||||
|
if (currentForm.getFormVersion() == null) {
|
||||||
|
currentForm.setFormVersion(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
FormDefinition savedForm = formDefinitionRepository.save(currentForm);
|
||||||
|
log.info("表单发布成功: id={}, version={}", savedForm.getId(), savedForm.getFormVersion());
|
||||||
|
|
||||||
|
return formDefinitionConverter.toDto(savedForm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FormDefinitionDTO getLatestByKey(String key) {
|
||||||
|
log.info("根据key获取最新版本表单: key={}", key);
|
||||||
|
|
||||||
|
FormDefinition formDefinition = formDefinitionRepository
|
||||||
|
.findFirstByKeyAndDeletedFalseOrderByFormVersionDesc(key)
|
||||||
|
.orElseThrow(() -> new BusinessException(com.qqchen.deploy.backend.framework.enums.ResponseCode.FORM_DEFINITION_NOT_FOUND));
|
||||||
|
|
||||||
|
return formDefinitionConverter.toDto(formDefinition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.service.impl;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.WorkflowCategoryDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.query.WorkflowCategoryQuery;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowCategory;
|
||||||
|
import com.qqchen.deploy.backend.workflow.service.IWorkflowCategoryService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流分类Service实现
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-10-24
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class WorkflowCategoryServiceImpl extends BaseServiceImpl<WorkflowCategory, WorkflowCategoryDTO, WorkflowCategoryQuery, Long> implements IWorkflowCategoryService {
|
||||||
|
}
|
||||||
|
|
||||||
@ -8,7 +8,6 @@ import com.qqchen.deploy.backend.workflow.dto.WorkflowTriggerTypeDTO;
|
|||||||
import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraph;
|
import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraph;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.query.WorkflowDefinitionQuery;
|
import com.qqchen.deploy.backend.workflow.dto.query.WorkflowDefinitionQuery;
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowCategoryEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowDefinitionStatusEnums;
|
import com.qqchen.deploy.backend.workflow.enums.WorkflowDefinitionStatusEnums;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowNodeInstanceStatusEnums;
|
import com.qqchen.deploy.backend.workflow.enums.WorkflowNodeInstanceStatusEnums;
|
||||||
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
|
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
|
||||||
@ -68,6 +67,11 @@ public class WorkflowDefinitionServiceImpl extends BaseServiceImpl<WorkflowDefin
|
|||||||
@Resource
|
@Resource
|
||||||
private IWorkflowDefinitionRepository workflowDefinitionRepository;
|
private IWorkflowDefinitionRepository workflowDefinitionRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.qqchen.deploy.backend.workflow.repository.IWorkflowCategoryRepository workflowCategoryRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.qqchen.deploy.backend.workflow.converter.WorkflowCategoryConverter workflowCategoryConverter;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private BpmnConverter bpmnConverter;
|
private BpmnConverter bpmnConverter;
|
||||||
@ -337,25 +341,11 @@ public class WorkflowDefinitionServiceImpl extends BaseServiceImpl<WorkflowDefin
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<WorkflowCategoryDTO> getWorkflowCategories() {
|
public List<WorkflowCategoryDTO> getWorkflowCategories() {
|
||||||
return Arrays.stream(WorkflowCategoryEnum.values())
|
// 从数据库查询启用的工作流分类,按排序字段排序
|
||||||
.map(category -> {
|
return workflowCategoryRepository.findAll().stream()
|
||||||
WorkflowCategoryDTO dto = new WorkflowCategoryDTO();
|
.filter(category -> category.getEnabled() && !category.getDeleted())
|
||||||
dto.setCode(category.name());
|
.sorted((a, b) -> a.getSort().compareTo(b.getSort()))
|
||||||
dto.setLable(category.getLabel());
|
.map(workflowCategoryConverter::toDto)
|
||||||
dto.setDescription(category.getDescription());
|
|
||||||
// 获取该类别支持的触发方式
|
|
||||||
dto.setSupportedTriggers(
|
|
||||||
category.getSupportedTriggers().stream()
|
|
||||||
.map(triggerType -> {
|
|
||||||
WorkflowTriggerTypeDTO triggerDto = new WorkflowTriggerTypeDTO();
|
|
||||||
triggerDto.setCode(triggerType.name());
|
|
||||||
triggerDto.setLable(triggerType.getLabel());
|
|
||||||
return triggerDto;
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList())
|
|
||||||
);
|
|
||||||
return dto;
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -91,7 +91,12 @@ VALUES
|
|||||||
(230, '集成工具', '/integration', 'Layout', 'ApiOutlined', 1, NULL, 6, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
(230, '集成工具', '/integration', 'Layout', 'ApiOutlined', 1, NULL, 6, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
(231, 'Jenkins集成', '/deploy/jenkins-manager', '/src/pages/Deploy/JenkinsManager/List', 'BuildOutlined', 2, 230, 10, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
(231, 'Jenkins集成', '/deploy/jenkins-manager', '/src/pages/Deploy/JenkinsManager/List', 'BuildOutlined', 2, 230, 10, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
(232, 'Git仓库', '/deploy/git-manager', '/src/pages/Deploy/GitManager/List', 'GithubOutlined', 2, 230, 20, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
(232, 'Git仓库', '/deploy/git-manager', '/src/pages/Deploy/GitManager/List', 'GithubOutlined', 2, 230, 20, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
(233, '外部服务', '/deploy/external', '/src/pages/Deploy/external/index', 'LinkOutlined', 2, 230, 30, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE);
|
(233, '外部服务', '/deploy/external', '/src/pages/Deploy/external/index', 'LinkOutlined', 2, 230, 30, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
|
|
||||||
|
-- 📄 表单管理
|
||||||
|
(300, '表单管理', '/form', 'Layout', 'FormOutlined', 1, NULL, 7, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
|
(301, '表单定义', '/form/definitions', '/src/pages/form/definitions/index', 'FileTextOutlined', 2, 300, 10, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
|
(302, '表单数据', '/form/data', '/src/pages/form/data/index', 'DatabaseOutlined', 2, 300, 20, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE);
|
||||||
|
|
||||||
-- 初始化角色数据
|
-- 初始化角色数据
|
||||||
INSERT INTO sys_role (id, create_time, code, name, type, description, sort)
|
INSERT INTO sys_role (id, create_time, code, name, type, description, sort)
|
||||||
@ -130,6 +135,7 @@ SELECT 1, id FROM sys_menu; -- 超级管理员拥有所有菜单权限
|
|||||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||||
VALUES
|
VALUES
|
||||||
(2, 1), (2, 2), (2, 3), (2, 4), (2, 5), -- 系统管理员拥有系统管理相关权限
|
(2, 1), (2, 2), (2, 3), (2, 4), (2, 5), -- 系统管理员拥有系统管理相关权限
|
||||||
|
(2, 300), (2, 301), (2, 302), -- 系统管理员拥有表单管理权限
|
||||||
(3, 233); -- 普通用户拥有外部服务权限
|
(3, 233); -- 普通用户拥有外部服务权限
|
||||||
|
|
||||||
-- 初始化权限模板
|
-- 初始化权限模板
|
||||||
@ -143,7 +149,9 @@ INSERT INTO sys_template_menu (template_id, menu_id)
|
|||||||
SELECT 1, id FROM sys_menu; -- 完整权限模板关联所有菜单
|
SELECT 1, id FROM sys_menu; -- 完整权限模板关联所有菜单
|
||||||
|
|
||||||
INSERT INTO sys_template_menu (template_id, menu_id)
|
INSERT INTO sys_template_menu (template_id, menu_id)
|
||||||
VALUES (2, 233); -- 基础权限模板关联外部服务菜单
|
VALUES
|
||||||
|
(2, 233), -- 基础权限模板关联外部服务菜单
|
||||||
|
(2, 300), (2, 302); -- 基础权限模板关联表单管理和表单数据(不包括表单定义)
|
||||||
|
|
||||||
-- 初始化权限数据
|
-- 初始化权限数据
|
||||||
INSERT INTO sys_permission (id, create_time, menu_id, code, name, type, sort)
|
INSERT INTO sys_permission (id, create_time, menu_id, code, name, type, sort)
|
||||||
@ -166,7 +174,21 @@ VALUES
|
|||||||
(11, NOW(), 233, 'system:external:update', '外部服务修改', 'FUNCTION', 3),
|
(11, NOW(), 233, 'system:external:update', '外部服务修改', 'FUNCTION', 3),
|
||||||
(12, NOW(), 233, 'system:external:delete', '外部服务删除', 'FUNCTION', 4),
|
(12, NOW(), 233, 'system:external:delete', '外部服务删除', 'FUNCTION', 4),
|
||||||
(13, NOW(), 233, 'system:external:test', '连接测试', 'FUNCTION', 5),
|
(13, NOW(), 233, 'system:external:test', '连接测试', 'FUNCTION', 5),
|
||||||
(14, NOW(), 233, 'system:external:sync', '数据同步', 'FUNCTION', 6);
|
(14, NOW(), 233, 'system:external:sync', '数据同步', 'FUNCTION', 6),
|
||||||
|
|
||||||
|
-- 表单定义权限
|
||||||
|
(15, NOW(), 301, 'form:definition:list', '表单定义列表', 'FUNCTION', 1),
|
||||||
|
(16, NOW(), 301, 'form:definition:create', '创建表单定义', 'FUNCTION', 2),
|
||||||
|
(17, NOW(), 301, 'form:definition:update', '修改表单定义', 'FUNCTION', 3),
|
||||||
|
(18, NOW(), 301, 'form:definition:delete', '删除表单定义', 'FUNCTION', 4),
|
||||||
|
(19, NOW(), 301, 'form:definition:publish', '发布表单', 'FUNCTION', 5),
|
||||||
|
|
||||||
|
-- 表单数据权限
|
||||||
|
(20, NOW(), 302, 'form:data:list', '表单数据列表', 'FUNCTION', 1),
|
||||||
|
(21, NOW(), 302, 'form:data:view', '查看表单数据', 'FUNCTION', 2),
|
||||||
|
(22, NOW(), 302, 'form:data:submit', '提交表单数据', 'FUNCTION', 3),
|
||||||
|
(23, NOW(), 302, 'form:data:delete', '删除表单数据', 'FUNCTION', 4),
|
||||||
|
(24, NOW(), 302, 'form:data:export', '导出表单数据', 'FUNCTION', 5);
|
||||||
|
|
||||||
-- --------------------------------------------------------------------------------------
|
-- --------------------------------------------------------------------------------------
|
||||||
-- 初始化外部系统数据
|
-- 初始化外部系统数据
|
||||||
@ -197,12 +219,23 @@ INSERT INTO sys_external_system (
|
|||||||
-- --------------------------------------------------------------------------------------
|
-- --------------------------------------------------------------------------------------
|
||||||
-- 初始化工作流相关数据
|
-- 初始化工作流相关数据
|
||||||
-- --------------------------------------------------------------------------------------
|
-- --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- 工作流分类初始数据
|
||||||
|
INSERT INTO workflow_category (name, code, description, icon, sort, supported_triggers, enabled, create_by, create_time, update_by, update_time, version, deleted)
|
||||||
|
VALUES
|
||||||
|
('脚本执行', 'SCRIPT_EXECUTION', '用于执行各类脚本的流程', 'CodeOutlined', 1, '["MANUAL","SCHEDULED"]', 1, 'system', NOW(), 'system', NOW(), 0, 0),
|
||||||
|
('应用部署', 'DEPLOYMENT', '用于应用部署的流程', 'DeploymentUnitOutlined', 2, '["MANUAL","SCHEDULED","APPROVAL"]', 1, 'system', NOW(), 'system', NOW(), 0, 0),
|
||||||
|
('数据同步', 'DATA_SYNC', '用于第三方系统数据同步的流程', 'SyncOutlined', 3, '["MANUAL","SCHEDULED"]', 1, 'system', NOW(), 'system', NOW(), 0, 0),
|
||||||
|
('配置同步', 'CONFIG_SYNC', '用于配置中心数据同步的流程', 'SettingOutlined', 4, '["MANUAL","APPROVAL"]', 1, 'system', NOW(), 'system', NOW(), 0, 0),
|
||||||
|
('审批流程', 'APPROVAL', '纯审批流程', 'AuditOutlined', 5, '["MANUAL"]', 1, 'system', NOW(), 'system', NOW(), 0, 0),
|
||||||
|
('其他', 'OTHER', '其他类型流程', 'AppstoreOutlined', 99, '["MANUAL"]', 1, 'system', NOW(), 'system', NOW(), 0, 0);
|
||||||
|
|
||||||
-- 工作流定义测试数据
|
-- 工作流定义测试数据
|
||||||
INSERT INTO workflow_definition (
|
INSERT INTO workflow_definition (
|
||||||
-- 基础信息
|
-- 基础信息
|
||||||
name, `key`, process_definition_id, flow_version, description, category, triggers,
|
name, `key`, process_definition_id, flow_version, description, category_id, form_definition_id,
|
||||||
-- 流程配置
|
-- 流程配置
|
||||||
graph, tags,
|
graph,
|
||||||
-- 流程属性
|
-- 流程属性
|
||||||
status,
|
status,
|
||||||
-- 审计字段
|
-- 审计字段
|
||||||
@ -210,7 +243,7 @@ INSERT INTO workflow_definition (
|
|||||||
) VALUES
|
) VALUES
|
||||||
-- 简单脚本流程:开始 -> 脚本任务 -> 结束
|
-- 简单脚本流程:开始 -> 脚本任务 -> 结束
|
||||||
(
|
(
|
||||||
'简单脚本流程', 'simple_script_flow', null, 1, '一个包含脚本任务的简单流程', 'SCRIPT_EXECUTION', null,
|
'简单脚本流程', 'simple_script_flow', null, 1, '一个包含脚本任务的简单流程', 1, null,
|
||||||
'{
|
'{
|
||||||
"nodes" : [ {
|
"nodes" : [ {
|
||||||
"id" : "startEvent1",
|
"id" : "startEvent1",
|
||||||
@ -371,14 +404,13 @@ INSERT INTO workflow_definition (
|
|||||||
"properties" : null
|
"properties" : null
|
||||||
} ]
|
} ]
|
||||||
}',
|
}',
|
||||||
'["simple","script","test"]',
|
|
||||||
'DRAFT',
|
'DRAFT',
|
||||||
NOW(), NOW(), NULL, NULL, 0, 0
|
NOW(), NOW(), NULL, NULL, 0, 0
|
||||||
),
|
),
|
||||||
|
|
||||||
-- 复杂业务流程:开始 -> 脚本任务A -> 脚本任务B -> 结束
|
-- 复杂业务流程:开始 -> 脚本任务A -> 脚本任务B -> 结束
|
||||||
(
|
(
|
||||||
'复杂业务流程', 'complex_business_flow', null, 1, '包含多个脚本任务节点的业务流程', 'SCRIPT_EXECUTION', null,
|
'复杂业务流程', 'complex_business_flow', null, 1, '包含多个脚本任务节点的业务流程', 1, null,
|
||||||
'{
|
'{
|
||||||
"nodes" : [ {
|
"nodes" : [ {
|
||||||
"id" : "startEvent1",
|
"id" : "startEvent1",
|
||||||
@ -602,7 +634,6 @@ INSERT INTO workflow_definition (
|
|||||||
"properties" : null
|
"properties" : null
|
||||||
} ]
|
} ]
|
||||||
}',
|
}',
|
||||||
'["complex","business","multi-task"]',
|
|
||||||
'DRAFT',
|
'DRAFT',
|
||||||
NOW(), NOW(), NULL, NULL, 0, 0
|
NOW(), NOW(), NULL, NULL, 0, 0
|
||||||
);
|
);
|
||||||
@ -835,3 +866,14 @@ VALUES
|
|||||||
-- 'DISABLED',
|
-- 'DISABLED',
|
||||||
-- '管理员邮件通知渠道(示例数据,请修改为实际配置)',
|
-- '管理员邮件通知渠道(示例数据,请修改为实际配置)',
|
||||||
-- 'admin', NOW(), 'admin', NOW(), 0, 0);
|
-- 'admin', NOW(), 'admin', NOW(), 0, 0);
|
||||||
|
|
||||||
|
-- --------------------------------------------------------------------------------------
|
||||||
|
-- 初始化表单分类数据
|
||||||
|
-- --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO form_category (name, code, description, icon, sort, enabled, create_by, create_time, update_by, update_time, version, deleted)
|
||||||
|
VALUES
|
||||||
|
('审批表单', 'APPROVAL', '用于审批流程的表单', 'CheckCircleOutlined', 1, 1, 'system', NOW(), 'system', NOW(), 0, 0),
|
||||||
|
('数据采集', 'DATA_COLLECTION', '用于数据采集的表单', 'DatabaseOutlined', 2, 1, 'system', NOW(), 'system', NOW(), 0, 0),
|
||||||
|
('问卷调查', 'SURVEY', '用于问卷调查的表单', 'FormOutlined', 3, 1, 'system', NOW(), 'system', NOW(), 0, 0),
|
||||||
|
('其他', 'OTHER', '其他类型的表单', 'FileOutlined', 99, 1, 'system', NOW(), 'system', NOW(), 0, 0);
|
||||||
|
|||||||
@ -371,6 +371,113 @@ CREATE TABLE deploy_repo_branch
|
|||||||
-- 工作流相关表
|
-- 工作流相关表
|
||||||
-- --------------------------------------------------------------------------------------
|
-- --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- 工作流分类表
|
||||||
|
CREATE TABLE workflow_category
|
||||||
|
(
|
||||||
|
-- 主键
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
|
||||||
|
-- 基础信息
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT '分类名称',
|
||||||
|
code VARCHAR(50) NOT NULL COMMENT '分类编码(唯一,如SCRIPT_EXECUTION)',
|
||||||
|
description VARCHAR(500) NULL COMMENT '分类描述',
|
||||||
|
icon VARCHAR(50) NULL COMMENT '图标',
|
||||||
|
sort INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||||
|
|
||||||
|
-- 支持的触发方式(JSON数组,如["MANUAL","SCHEDULED"])
|
||||||
|
supported_triggers JSON NULL COMMENT '支持的触发方式列表',
|
||||||
|
|
||||||
|
-- 状态
|
||||||
|
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||||
|
|
||||||
|
-- 审计字段
|
||||||
|
create_by VARCHAR(255) NULL COMMENT '创建人',
|
||||||
|
create_time DATETIME(6) NULL COMMENT '创建时间',
|
||||||
|
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||||
|
update_by VARCHAR(255) NULL COMMENT '更新人',
|
||||||
|
update_time DATETIME(6) NULL COMMENT '更新时间',
|
||||||
|
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||||
|
|
||||||
|
-- 约束和索引
|
||||||
|
UNIQUE KEY uk_code (code),
|
||||||
|
INDEX idx_enabled (enabled),
|
||||||
|
INDEX idx_deleted (deleted),
|
||||||
|
INDEX idx_sort (sort)
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
|
||||||
|
COMMENT = '工作流分类表';
|
||||||
|
|
||||||
|
CREATE TABLE form_category
|
||||||
|
(
|
||||||
|
-- 主键
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
|
||||||
|
-- 基础信息
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT '分类名称',
|
||||||
|
code VARCHAR(50) NOT NULL COMMENT '分类编码(唯一)',
|
||||||
|
description VARCHAR(500) NULL COMMENT '分类描述',
|
||||||
|
icon VARCHAR(50) NULL COMMENT '图标',
|
||||||
|
sort INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||||
|
|
||||||
|
-- 状态
|
||||||
|
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||||
|
|
||||||
|
-- 审计字段
|
||||||
|
create_by VARCHAR(255) NULL COMMENT '创建人',
|
||||||
|
create_time DATETIME(6) NULL COMMENT '创建时间',
|
||||||
|
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||||
|
update_by VARCHAR(255) NULL COMMENT '更新人',
|
||||||
|
update_time DATETIME(6) NULL COMMENT '更新时间',
|
||||||
|
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||||
|
|
||||||
|
-- 约束和索引
|
||||||
|
UNIQUE KEY uk_code (code),
|
||||||
|
INDEX idx_enabled (enabled),
|
||||||
|
INDEX idx_deleted (deleted),
|
||||||
|
INDEX idx_sort (sort)
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
|
||||||
|
COMMENT = '表单分类表';
|
||||||
|
|
||||||
|
-- 表单定义表
|
||||||
|
CREATE TABLE form_definition
|
||||||
|
(
|
||||||
|
-- 主键
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
|
||||||
|
-- 基础信息
|
||||||
|
name VARCHAR(255) NOT NULL COMMENT '表单名称',
|
||||||
|
`key` VARCHAR(255) NOT NULL COMMENT '表单标识(业务唯一)',
|
||||||
|
form_version INT NOT NULL DEFAULT 1 COMMENT '表单版本号',
|
||||||
|
category_id BIGINT NULL COMMENT '表单分类ID(外键关联form_category)',
|
||||||
|
description TEXT NULL COMMENT '表单描述',
|
||||||
|
|
||||||
|
-- 表单配置
|
||||||
|
`schema` JSON NOT NULL COMMENT '表单Schema(前端设计器导出的JSON结构)',
|
||||||
|
tags JSON NULL COMMENT '标签(用于分类和搜索)',
|
||||||
|
|
||||||
|
-- 表单属性
|
||||||
|
status VARCHAR(50) NOT NULL DEFAULT 'DRAFT' COMMENT '状态(DRAFT-草稿、PUBLISHED-已发布、DISABLED-已禁用)',
|
||||||
|
is_template BIT NOT NULL DEFAULT 0 COMMENT '是否为模板',
|
||||||
|
|
||||||
|
-- 审计字段
|
||||||
|
create_by VARCHAR(255) NULL COMMENT '创建人',
|
||||||
|
create_time DATETIME(6) NULL COMMENT '创建时间',
|
||||||
|
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||||
|
update_by VARCHAR(255) NULL COMMENT '更新人',
|
||||||
|
update_time DATETIME(6) NULL COMMENT '更新时间',
|
||||||
|
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||||
|
|
||||||
|
-- 约束和索引
|
||||||
|
UNIQUE KEY uk_key_version (`key`, form_version),
|
||||||
|
INDEX idx_category_id (category_id),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_is_template (is_template),
|
||||||
|
INDEX idx_deleted (deleted),
|
||||||
|
|
||||||
|
-- 外键约束
|
||||||
|
CONSTRAINT fk_form_definition_category FOREIGN KEY (category_id) REFERENCES form_category(id)
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
|
||||||
|
COMMENT = '表单定义表';
|
||||||
|
|
||||||
-- 工作流定义表
|
-- 工作流定义表
|
||||||
CREATE TABLE workflow_definition
|
CREATE TABLE workflow_definition
|
||||||
(
|
(
|
||||||
@ -380,8 +487,8 @@ CREATE TABLE workflow_definition
|
|||||||
-- 基础信息
|
-- 基础信息
|
||||||
name VARCHAR(255) NOT NULL COMMENT '流程名称',
|
name VARCHAR(255) NOT NULL COMMENT '流程名称',
|
||||||
`key` VARCHAR(255) NOT NULL COMMENT '流程标识',
|
`key` VARCHAR(255) NOT NULL COMMENT '流程标识',
|
||||||
category VARCHAR(100) COMMENT '流程分类',
|
category_id BIGINT NULL COMMENT '流程分类ID(外键关联workflow_category)',
|
||||||
triggers VARCHAR(200) COMMENT '流程分类',
|
form_definition_id BIGINT NULL COMMENT '启动表单ID(外键关联form_definition)',
|
||||||
process_definition_id VARCHAR(100) NULL COMMENT '工作流定义ID',
|
process_definition_id VARCHAR(100) NULL COMMENT '工作流定义ID',
|
||||||
flow_version INT NOT NULL COMMENT '流程版本',
|
flow_version INT NOT NULL COMMENT '流程版本',
|
||||||
description TEXT COMMENT '流程描述',
|
description TEXT COMMENT '流程描述',
|
||||||
@ -389,7 +496,6 @@ CREATE TABLE workflow_definition
|
|||||||
-- 流程配置
|
-- 流程配置
|
||||||
bpmn_xml TEXT COMMENT 'BPMN XML内容',
|
bpmn_xml TEXT COMMENT 'BPMN XML内容',
|
||||||
graph JSON COMMENT '流程图数据,包含节点和连线的位置、样式等信息',
|
graph JSON COMMENT '流程图数据,包含节点和连线的位置、样式等信息',
|
||||||
tags JSON COMMENT '流程标签',
|
|
||||||
|
|
||||||
-- 流程属性
|
-- 流程属性
|
||||||
status VARCHAR(50) NOT NULL COMMENT '流程状态(DRAFT-草稿、PUBLISHED-已发布、DISABLED-已禁用)',
|
status VARCHAR(50) NOT NULL COMMENT '流程状态(DRAFT-草稿、PUBLISHED-已发布、DISABLED-已禁用)',
|
||||||
@ -402,8 +508,14 @@ CREATE TABLE workflow_definition
|
|||||||
update_time DATETIME(6) NULL COMMENT '更新时间',
|
update_time DATETIME(6) NULL COMMENT '更新时间',
|
||||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||||
|
|
||||||
-- 约束
|
-- 约束和索引
|
||||||
UNIQUE KEY uk_key_version (`key`, flow_version)
|
UNIQUE KEY uk_key_version (`key`, flow_version),
|
||||||
|
INDEX idx_category_id (category_id),
|
||||||
|
INDEX idx_form_definition_id (form_definition_id),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_deleted (deleted),
|
||||||
|
CONSTRAINT fk_workflow_definition_category FOREIGN KEY (category_id) REFERENCES workflow_category(id),
|
||||||
|
CONSTRAINT fk_workflow_definition_form FOREIGN KEY (form_definition_id) REFERENCES form_definition(id)
|
||||||
) ENGINE = InnoDB
|
) ENGINE = InnoDB
|
||||||
DEFAULT CHARSET = utf8mb4
|
DEFAULT CHARSET = utf8mb4
|
||||||
COLLATE = utf8mb4_unicode_ci COMMENT ='工作流定义表';
|
COLLATE = utf8mb4_unicode_ci COMMENT ='工作流定义表';
|
||||||
@ -689,3 +801,51 @@ CREATE TABLE sys_notification_channel (
|
|||||||
INDEX idx_deleted (deleted)
|
INDEX idx_deleted (deleted)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知渠道配置表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知渠道配置表';
|
||||||
|
|
||||||
|
-- 表单数据表
|
||||||
|
CREATE TABLE form_data
|
||||||
|
(
|
||||||
|
-- 主键
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
|
||||||
|
-- 关联表单定义
|
||||||
|
form_definition_id BIGINT NOT NULL COMMENT '表单定义ID',
|
||||||
|
form_key VARCHAR(255) NOT NULL COMMENT '表单标识(冗余存储,避免JOIN)',
|
||||||
|
form_version INT NOT NULL COMMENT '表单版本(冗余存储,用于历史追溯)',
|
||||||
|
category_id BIGINT NULL COMMENT '表单分类ID(冗余存储,便于统计和查询)',
|
||||||
|
|
||||||
|
-- 业务关联(松耦合)
|
||||||
|
business_key VARCHAR(255) NULL COMMENT '业务标识(如工作流实例ID、订单号等)',
|
||||||
|
business_type VARCHAR(50) NULL COMMENT '业务类型(WORKFLOW-工作流、ORDER-订单、STANDALONE-独立表单)',
|
||||||
|
|
||||||
|
-- 表单数据
|
||||||
|
data JSON NOT NULL COMMENT '表单填写数据(用户提交的实际数据)',
|
||||||
|
schema_snapshot JSON NOT NULL COMMENT '表单Schema快照(用于历史追溯,确保数据可还原)',
|
||||||
|
|
||||||
|
-- 提交信息
|
||||||
|
submitter VARCHAR(255) NULL COMMENT '提交人',
|
||||||
|
submit_time DATETIME(6) NULL COMMENT '提交时间',
|
||||||
|
|
||||||
|
-- 状态
|
||||||
|
status VARCHAR(50) NOT NULL DEFAULT 'SUBMITTED' COMMENT '状态(DRAFT-草稿、SUBMITTED-已提交、COMPLETED-已完成、REJECTED-已拒绝)',
|
||||||
|
|
||||||
|
-- 审计字段
|
||||||
|
create_by VARCHAR(255) NULL COMMENT '创建人',
|
||||||
|
create_time DATETIME(6) NULL COMMENT '创建时间',
|
||||||
|
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||||
|
update_by VARCHAR(255) NULL COMMENT '更新人',
|
||||||
|
update_time DATETIME(6) NULL COMMENT '更新时间',
|
||||||
|
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
INDEX idx_form_definition_id (form_definition_id),
|
||||||
|
INDEX idx_form_key (form_key),
|
||||||
|
INDEX idx_category_id (category_id),
|
||||||
|
INDEX idx_business_key (business_key),
|
||||||
|
INDEX idx_business_type (business_type),
|
||||||
|
INDEX idx_submitter (submitter),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_submit_time (submit_time),
|
||||||
|
INDEX idx_deleted (deleted)
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
|
||||||
|
COMMENT = '表单数据表';
|
||||||
|
|
||||||
|
|||||||
@ -241,3 +241,7 @@ workflow.variable.serialize.error=\u5DE5\u4F5C\u6D41\u53D8\u91CF\u5E8F\u5217\u53
|
|||||||
workflow.variable.deserialize.error=\u5DE5\u4F5C\u6D41\u53D8\u91CF\u53CD\u5E8F\u5217\u5316\u5931\u8D25: {0}
|
workflow.variable.deserialize.error=\u5DE5\u4F5C\u6D41\u53D8\u91CF\u53CD\u5E8F\u5217\u5316\u5931\u8D25: {0}
|
||||||
|
|
||||||
workflow.config.error=\u5DE5\u4F5C\u6D41\u914D\u7F6E\u9519\u8BEF: {0}
|
workflow.config.error=\u5DE5\u4F5C\u6D41\u914D\u7F6E\u9519\u8BEF: {0}
|
||||||
|
|
||||||
|
# \u8868\u5355\u7BA1\u7406\u76F8\u5173\u9519\u8BEF\u7801 (2800-2899)
|
||||||
|
form.definition.not.found=\u8868\u5355\u5B9A\u4E49\u4E0D\u5B58\u5728\u6216\u5DF2\u5220\u9664
|
||||||
|
form.data.not.found=\u8868\u5355\u6570\u636E\u4E0D\u5B58\u5728\u6216\u5DF2\u5220\u9664
|
||||||
199
frontend/src/components/ui/dropdown-menu.tsx
Normal file
199
frontend/src/components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
))
|
||||||
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSubContent.displayName =
|
||||||
|
DropdownMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
))
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
))
|
||||||
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
))
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
}
|
||||||
46
frontend/src/pages/Form/Category/service.ts
Normal file
46
frontend/src/pages/Form/Category/service.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import type {
|
||||||
|
FormCategoryQuery,
|
||||||
|
FormCategoryResponse,
|
||||||
|
FormCategoryRequest,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const BASE_URL = '/api/v1/forms/categories';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询分类
|
||||||
|
*/
|
||||||
|
export const getCategories = (params?: FormCategoryQuery) =>
|
||||||
|
request.get<Page<FormCategoryResponse>>(`${BASE_URL}/page`, { params });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询分类详情
|
||||||
|
*/
|
||||||
|
export const getCategoryById = (id: number) =>
|
||||||
|
request.get<FormCategoryResponse>(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有启用的分类(常用)
|
||||||
|
*/
|
||||||
|
export const getEnabledCategories = () =>
|
||||||
|
request.get<FormCategoryResponse[]>(`${BASE_URL}/enabled`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分类
|
||||||
|
*/
|
||||||
|
export const createCategory = (data: FormCategoryRequest) =>
|
||||||
|
request.post<FormCategoryResponse>(BASE_URL, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分类
|
||||||
|
*/
|
||||||
|
export const updateCategory = (id: number, data: FormCategoryRequest) =>
|
||||||
|
request.put<FormCategoryResponse>(`${BASE_URL}/${id}`, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分类
|
||||||
|
*/
|
||||||
|
export const deleteCategory = (id: number) =>
|
||||||
|
request.delete(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
40
frontend/src/pages/Form/Category/types.ts
Normal file
40
frontend/src/pages/Form/Category/types.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { BaseQuery } from '@/types/base';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类查询参数
|
||||||
|
*/
|
||||||
|
export interface FormCategoryQuery extends BaseQuery {
|
||||||
|
name?: string;
|
||||||
|
code?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类响应
|
||||||
|
*/
|
||||||
|
export interface FormCategoryResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
sort: number;
|
||||||
|
enabled: boolean;
|
||||||
|
createBy?: string;
|
||||||
|
createTime?: string;
|
||||||
|
updateBy?: string;
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类创建/更新请求
|
||||||
|
*/
|
||||||
|
export interface FormCategoryRequest {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
sort?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
150
frontend/src/pages/Form/Data/Detail.tsx
Normal file
150
frontend/src/pages/Form/Data/Detail.tsx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { FormRenderer } from '@/components/FormDesigner';
|
||||||
|
import { ArrowLeft, Loader2 } from 'lucide-react';
|
||||||
|
import { getFormDataById } from './service';
|
||||||
|
import type { FormDataResponse, FormDataStatus, FormDataBusinessType } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据详情页
|
||||||
|
*/
|
||||||
|
const FormDataDetail: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState<FormDataResponse | null>(null);
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
loadData(Number(id));
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const loadData = async (dataId: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await getFormDataById(dataId);
|
||||||
|
setData(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载表单数据失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回列表
|
||||||
|
const handleBack = () => {
|
||||||
|
navigate('/form/data');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 状态徽章
|
||||||
|
const getStatusBadge = (status: FormDataStatus) => {
|
||||||
|
const statusMap: Record<FormDataStatus, { variant: 'default' | 'secondary' | 'destructive' | 'success' | 'outline'; text: string }> = {
|
||||||
|
DRAFT: { variant: 'outline', text: '草稿' },
|
||||||
|
SUBMITTED: { variant: 'success', text: '已提交' },
|
||||||
|
COMPLETED: { variant: 'default', text: '已完成' },
|
||||||
|
};
|
||||||
|
const statusInfo = statusMap[status];
|
||||||
|
return <Badge variant={statusInfo.variant}>{statusInfo.text}</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 业务类型徽章
|
||||||
|
const getBusinessTypeBadge = (type: FormDataBusinessType) => {
|
||||||
|
const typeMap: Record<FormDataBusinessType, { variant: 'default' | 'secondary' | 'outline'; text: string }> = {
|
||||||
|
STANDALONE: { variant: 'outline', text: '独立' },
|
||||||
|
WORKFLOW: { variant: 'default', text: '工作流' },
|
||||||
|
ORDER: { variant: 'secondary', text: '订单' },
|
||||||
|
};
|
||||||
|
const typeInfo = typeMap[type];
|
||||||
|
return <Badge variant={typeInfo.variant}>{typeInfo.text}</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 描述项组件
|
||||||
|
const DescriptionItem: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => (
|
||||||
|
<div className="flex py-3 border-b last:border-b-0">
|
||||||
|
<div className="w-32 text-muted-foreground flex-shrink-0">{label}</div>
|
||||||
|
<div className="flex-1 font-medium">{value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-12">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin mr-2" />
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-12">
|
||||||
|
<div className="text-center text-muted-foreground">数据不存在</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<Card className="mb-4">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle>表单数据详情</CardTitle>
|
||||||
|
<Button variant="outline" onClick={handleBack}>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
返回列表
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-0">
|
||||||
|
<DescriptionItem label="表单标识" value={data.formKey} />
|
||||||
|
<DescriptionItem label="表单版本" value={`v${data.formVersion}`} />
|
||||||
|
<DescriptionItem label="业务类型" value={getBusinessTypeBadge(data.businessType)} />
|
||||||
|
<DescriptionItem label="业务标识" value={data.businessKey || '-'} />
|
||||||
|
<DescriptionItem label="提交人" value={data.submitter || '-'} />
|
||||||
|
<DescriptionItem label="提交时间" value={data.submitTime || '-'} />
|
||||||
|
<DescriptionItem label="状态" value={getStatusBadge(data.status)} />
|
||||||
|
<DescriptionItem label="创建时间" value={data.createTime || '-'} />
|
||||||
|
<DescriptionItem label="更新时间" value={data.updateTime || '-'} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>表单数据</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{/* 使用 FormRenderer 以只读模式展示数据 */}
|
||||||
|
<FormRenderer
|
||||||
|
schema={data.schemaSnapshot}
|
||||||
|
value={data.data}
|
||||||
|
readonly={true}
|
||||||
|
showSubmit={false}
|
||||||
|
showCancel={true}
|
||||||
|
cancelText="返回"
|
||||||
|
onCancel={handleBack}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormDataDetail;
|
||||||
|
|
||||||
408
frontend/src/pages/Form/Data/index.tsx
Normal file
408
frontend/src/pages/Form/Data/index.tsx
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||||
|
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { DataTablePagination } from '@/components/ui/pagination';
|
||||||
|
import {
|
||||||
|
Loader2, Search, Eye, Trash2, Download, Folder,
|
||||||
|
Activity, Clock, CheckCircle2, FileCheck, Database
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { getFormDataList, deleteFormData, exportFormData } from './service';
|
||||||
|
import { getEnabledCategories } from '../Category/service';
|
||||||
|
import type { FormDataResponse, FormDataStatus, FormDataBusinessType } from './types';
|
||||||
|
import type { FormCategoryResponse } from '../Category/types';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据列表页
|
||||||
|
*/
|
||||||
|
const FormDataList: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState<Page<FormDataResponse> | null>(null);
|
||||||
|
const [categories, setCategories] = useState<FormCategoryResponse[]>([]);
|
||||||
|
const [query, setQuery] = useState({
|
||||||
|
pageNum: DEFAULT_CURRENT - 1,
|
||||||
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
|
formDefinitionId: searchParams.get('formDefinitionId') ? Number(searchParams.get('formDefinitionId')) : undefined,
|
||||||
|
businessKey: '',
|
||||||
|
categoryId: undefined as number | undefined,
|
||||||
|
status: undefined as FormDataStatus | undefined,
|
||||||
|
businessType: undefined as FormDataBusinessType | undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载分类列表
|
||||||
|
const loadCategories = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getEnabledCategories();
|
||||||
|
setCategories(result || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分类失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await getFormDataList(query);
|
||||||
|
setData(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载表单数据失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadCategories();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
setQuery(prev => ({
|
||||||
|
...prev,
|
||||||
|
pageNum: 0,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const handleReset = () => {
|
||||||
|
setQuery({
|
||||||
|
pageNum: 0,
|
||||||
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
|
formDefinitionId: undefined,
|
||||||
|
businessKey: '',
|
||||||
|
categoryId: undefined,
|
||||||
|
status: undefined,
|
||||||
|
businessType: undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据分类 ID 获取分类信息
|
||||||
|
const getCategoryInfo = (categoryId?: number) => {
|
||||||
|
return categories.find(cat => cat.id === categoryId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleView = (record: FormDataResponse) => {
|
||||||
|
navigate(`/form/data/${record.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = async (record: FormDataResponse) => {
|
||||||
|
if (!confirm('确定要删除该数据吗?')) return;
|
||||||
|
try {
|
||||||
|
await deleteFormData(record.id);
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除数据失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
await exportFormData(query);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出数据失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 状态徽章
|
||||||
|
const getStatusBadge = (status: FormDataStatus) => {
|
||||||
|
const statusMap: Record<FormDataStatus, {
|
||||||
|
variant: 'default' | 'secondary' | 'destructive' | 'success' | 'outline';
|
||||||
|
text: string;
|
||||||
|
icon: React.ElementType
|
||||||
|
}> = {
|
||||||
|
DRAFT: { variant: 'outline', text: '草稿', icon: Clock },
|
||||||
|
SUBMITTED: { variant: 'success', text: '已提交', icon: CheckCircle2 },
|
||||||
|
COMPLETED: { variant: 'default', text: '已完成', icon: FileCheck },
|
||||||
|
};
|
||||||
|
const statusInfo = statusMap[status];
|
||||||
|
const Icon = statusInfo.icon;
|
||||||
|
return (
|
||||||
|
<Badge variant={statusInfo.variant} className="flex items-center gap-1">
|
||||||
|
<Icon className="h-3 w-3" />
|
||||||
|
{statusInfo.text}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 业务类型徽章
|
||||||
|
const getBusinessTypeBadge = (type: FormDataBusinessType) => {
|
||||||
|
const typeMap: Record<FormDataBusinessType, { variant: 'default' | 'secondary' | 'outline'; text: string }> = {
|
||||||
|
STANDALONE: { variant: 'outline', text: '独立' },
|
||||||
|
WORKFLOW: { variant: 'default', text: '工作流' },
|
||||||
|
ORDER: { variant: 'secondary', text: '订单' },
|
||||||
|
};
|
||||||
|
const typeInfo = typeMap[type];
|
||||||
|
return <Badge variant={typeInfo.variant}>{typeInfo.text}</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
const total = data?.totalElements || 0;
|
||||||
|
const draftCount = data?.content?.filter(d => d.status === 'DRAFT').length || 0;
|
||||||
|
const submittedCount = data?.content?.filter(d => d.status === 'SUBMITTED').length || 0;
|
||||||
|
const completedCount = data?.content?.filter(d => d.status === 'COMPLETED').length || 0;
|
||||||
|
return { total, draftCount, submittedCount, completedCount };
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const pageCount = data?.totalElements ? Math.ceil(data.totalElements / query.pageSize) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-foreground">表单数据管理</h1>
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
查看和管理用户提交的表单数据,支持按分类、状态、业务类型筛选。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
|
<Card className="bg-gradient-to-br from-blue-500/10 to-blue-500/5 border-blue-500/20">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-blue-700">总数据量</CardTitle>
|
||||||
|
<Activity className="h-4 w-4 text-blue-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.total}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">全部表单数据</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-gradient-to-br from-yellow-500/10 to-yellow-500/5 border-yellow-500/20">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-yellow-700">草稿</CardTitle>
|
||||||
|
<Clock className="h-4 w-4 text-yellow-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.draftCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">暂存的数据</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-gradient-to-br from-green-500/10 to-green-500/5 border-green-500/20">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-green-700">已提交</CardTitle>
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.submittedCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">待处理的数据</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-gradient-to-br from-purple-500/10 to-purple-500/5 border-purple-500/20">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-purple-700">已完成</CardTitle>
|
||||||
|
<FileCheck className="h-4 w-4 text-purple-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.completedCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">已处理完成</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>数据列表</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<div className="flex flex-wrap items-center gap-4 mb-4">
|
||||||
|
<div className="flex-1 max-w-md">
|
||||||
|
<Input
|
||||||
|
placeholder="搜索业务标识"
|
||||||
|
value={query.businessKey}
|
||||||
|
onChange={(e) => setQuery(prev => ({ ...prev, businessKey: e.target.value }))}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={query.categoryId?.toString() || undefined}
|
||||||
|
onValueChange={(value) => setQuery(prev => ({ ...prev, categoryId: Number(value) }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[160px] h-9">
|
||||||
|
<SelectValue placeholder="全部分类" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{categories.map(cat => (
|
||||||
|
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{cat.icon && <Folder className="h-3.5 w-3.5" />}
|
||||||
|
{cat.name}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={query.businessType || undefined}
|
||||||
|
onValueChange={(value) => setQuery(prev => ({ ...prev, businessType: value as FormDataBusinessType }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[140px] h-9">
|
||||||
|
<SelectValue placeholder="全部类型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="STANDALONE">独立</SelectItem>
|
||||||
|
<SelectItem value="WORKFLOW">工作流</SelectItem>
|
||||||
|
<SelectItem value="ORDER">订单</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={query.status || undefined}
|
||||||
|
onValueChange={(value) => setQuery(prev => ({ ...prev, status: value as FormDataStatus }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[130px] h-9">
|
||||||
|
<SelectValue placeholder="全部状态" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="DRAFT">草稿</SelectItem>
|
||||||
|
<SelectItem value="SUBMITTED">已提交</SelectItem>
|
||||||
|
<SelectItem value="COMPLETED">已完成</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button onClick={handleSearch} className="h-9">
|
||||||
|
<Search className="h-4 w-4 mr-2" />
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleReset} className="h-9">
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleExport} className="h-9">
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 表格 */}
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[180px]">表单标识</TableHead>
|
||||||
|
<TableHead className="w-[120px]">分类</TableHead>
|
||||||
|
<TableHead className="w-[100px]">业务类型</TableHead>
|
||||||
|
<TableHead className="w-[150px]">业务标识</TableHead>
|
||||||
|
<TableHead className="w-[100px]">提交人</TableHead>
|
||||||
|
<TableHead className="w-[180px]">提交时间</TableHead>
|
||||||
|
<TableHead className="w-[100px]">状态</TableHead>
|
||||||
|
<TableHead className="text-right">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="h-24 text-center">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||||
|
<span className="text-sm text-muted-foreground">加载中...</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : data?.content && data.content.length > 0 ? (
|
||||||
|
data.content.map((record) => {
|
||||||
|
const categoryInfo = getCategoryInfo(record.categoryId);
|
||||||
|
return (
|
||||||
|
<TableRow key={record.id} className="hover:bg-muted/50">
|
||||||
|
<TableCell>
|
||||||
|
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">
|
||||||
|
{record.formKey}
|
||||||
|
</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{categoryInfo ? (
|
||||||
|
<Badge variant="outline" className="flex items-center gap-1 w-fit">
|
||||||
|
{categoryInfo.icon && <Folder className="h-3 w-3" />}
|
||||||
|
{categoryInfo.name}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline">未分类</Badge>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{getBusinessTypeBadge(record.businessType)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm">{record.businessKey || '-'}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm">{record.submitter || '匿名'}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm">
|
||||||
|
{record.submitTime ? dayjs(record.submitTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{getStatusBadge(record.status)}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleView(record)}
|
||||||
|
title="查看详情"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(record)}
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
title="删除"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="h-24 text-center">
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
|
<Database className="w-16 h-16 mb-4 text-muted-foreground/50" />
|
||||||
|
<div className="text-lg font-semibold mb-2">暂无表单数据</div>
|
||||||
|
<div className="text-sm">用户提交的表单数据将在此显示。</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{pageCount > 1 && (
|
||||||
|
<DataTablePagination
|
||||||
|
pageIndex={query.pageNum + 1}
|
||||||
|
pageSize={query.pageSize}
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={(page) => setQuery(prev => ({
|
||||||
|
...prev,
|
||||||
|
pageNum: page - 1
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormDataList;
|
||||||
|
|
||||||
53
frontend/src/pages/Form/Data/service.ts
Normal file
53
frontend/src/pages/Form/Data/service.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import type {
|
||||||
|
FormDataQuery,
|
||||||
|
FormDataResponse,
|
||||||
|
FormDataSubmitRequest,
|
||||||
|
FormDataUpdateRequest,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const BASE_URL = '/api/v1/forms/data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询表单数据列表
|
||||||
|
*/
|
||||||
|
export const getFormDataList = (params?: FormDataQuery) =>
|
||||||
|
request.get<Page<FormDataResponse>>(`${BASE_URL}/page`, { params });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表单数据详情
|
||||||
|
*/
|
||||||
|
export const getFormDataById = (id: number) =>
|
||||||
|
request.get<FormDataResponse>(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交表单数据
|
||||||
|
*/
|
||||||
|
export const submitFormData = (data: FormDataSubmitRequest) =>
|
||||||
|
request.post<FormDataResponse>(BASE_URL, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新表单数据
|
||||||
|
*/
|
||||||
|
export const updateFormData = (id: number, data: FormDataUpdateRequest) =>
|
||||||
|
request.put<FormDataResponse>(`${BASE_URL}/${id}`, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除表单数据
|
||||||
|
*/
|
||||||
|
export const deleteFormData = (id: number) =>
|
||||||
|
request.delete(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除表单数据
|
||||||
|
*/
|
||||||
|
export const batchDeleteFormData = (ids: number[]) =>
|
||||||
|
request.post(`${BASE_URL}/batch-delete`, { ids });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出表单数据
|
||||||
|
*/
|
||||||
|
export const exportFormData = (params?: FormDataQuery) =>
|
||||||
|
request.download(`${BASE_URL}/export`, undefined, { params });
|
||||||
|
|
||||||
70
frontend/src/pages/Form/Data/types.ts
Normal file
70
frontend/src/pages/Form/Data/types.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { BaseQuery } from '@/types/base';
|
||||||
|
import type { FormSchema } from '@/components/FormDesigner';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务类型
|
||||||
|
*/
|
||||||
|
export type FormDataBusinessType = 'STANDALONE' | 'WORKFLOW' | 'ORDER';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据状态
|
||||||
|
*/
|
||||||
|
export type FormDataStatus = 'DRAFT' | 'SUBMITTED' | 'COMPLETED';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据查询参数
|
||||||
|
*/
|
||||||
|
export interface FormDataQuery extends BaseQuery {
|
||||||
|
formDefinitionId?: number;
|
||||||
|
formKey?: string;
|
||||||
|
categoryId?: number; // 分类ID(筛选)
|
||||||
|
businessType?: FormDataBusinessType;
|
||||||
|
businessKey?: string;
|
||||||
|
submitter?: string;
|
||||||
|
status?: FormDataStatus;
|
||||||
|
submitTimeStart?: string;
|
||||||
|
submitTimeEnd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据响应
|
||||||
|
*/
|
||||||
|
export interface FormDataResponse {
|
||||||
|
id: number;
|
||||||
|
formDefinitionId: number;
|
||||||
|
formKey: string;
|
||||||
|
formVersion: number;
|
||||||
|
categoryId?: number; // 分类ID
|
||||||
|
businessKey?: string;
|
||||||
|
businessType: FormDataBusinessType;
|
||||||
|
data: Record<string, any>;
|
||||||
|
schemaSnapshot: FormSchema;
|
||||||
|
submitter?: string;
|
||||||
|
submitTime?: string;
|
||||||
|
status: FormDataStatus;
|
||||||
|
createBy?: string;
|
||||||
|
createTime?: string;
|
||||||
|
updateBy?: string;
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据提交请求
|
||||||
|
*/
|
||||||
|
export interface FormDataSubmitRequest {
|
||||||
|
formDefinitionId: number;
|
||||||
|
formKey: string;
|
||||||
|
businessType?: FormDataBusinessType;
|
||||||
|
businessKey?: string;
|
||||||
|
data: Record<string, any>;
|
||||||
|
status?: FormDataStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据更新请求
|
||||||
|
*/
|
||||||
|
export interface FormDataUpdateRequest {
|
||||||
|
data: Record<string, any>;
|
||||||
|
status?: FormDataStatus;
|
||||||
|
}
|
||||||
|
|
||||||
107
frontend/src/pages/Form/Definition/Designer.tsx
Normal file
107
frontend/src/pages/Form/Definition/Designer.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { FormDesigner } from '@/components/FormDesigner';
|
||||||
|
import type { FormSchema } from '@/components/FormDesigner';
|
||||||
|
import { ArrowLeft, Workflow } from 'lucide-react';
|
||||||
|
import { getDefinitionById, updateDefinition } from './service';
|
||||||
|
import type { FormDefinitionRequest } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单设计器页面
|
||||||
|
*/
|
||||||
|
const FormDesignerPage: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const [formSchema, setFormSchema] = useState<FormSchema | null>(null);
|
||||||
|
const [formDefinition, setFormDefinition] = useState<any>(null);
|
||||||
|
|
||||||
|
// 加载表单定义
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
loadFormDefinition(Number(id));
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const loadFormDefinition = async (definitionId: number) => {
|
||||||
|
try {
|
||||||
|
const result = await getDefinitionById(definitionId);
|
||||||
|
setFormDefinition(result);
|
||||||
|
setFormSchema(result.schema);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载表单定义失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存表单(只更新 schema,不修改基本信息)
|
||||||
|
const handleSave = async (schema: FormSchema) => {
|
||||||
|
if (!id || !formDefinition) {
|
||||||
|
alert('表单信息加载失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request: FormDefinitionRequest = {
|
||||||
|
name: formDefinition.name,
|
||||||
|
key: formDefinition.key,
|
||||||
|
categoryId: formDefinition.categoryId,
|
||||||
|
description: formDefinition.description,
|
||||||
|
isTemplate: formDefinition.isTemplate,
|
||||||
|
schema,
|
||||||
|
status: formDefinition.status || 'DRAFT',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateDefinition(Number(id), request);
|
||||||
|
alert('保存成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存表单失败:', error);
|
||||||
|
alert('保存失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回列表
|
||||||
|
const handleBack = () => {
|
||||||
|
navigate('/form/definitions');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6 p-6">
|
||||||
|
{/* 页面标题 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 rounded-lg bg-blue-500/10">
|
||||||
|
<Workflow className="h-6 w-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
|
{formDefinition?.name || '表单设计'}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{formDefinition?.description || '拖拽左侧组件到画布,配置字段属性和验证规则'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" onClick={handleBack}>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
返回列表
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 表单设计器区域 */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="rounded-lg">
|
||||||
|
<FormDesigner
|
||||||
|
value={formSchema || undefined}
|
||||||
|
onChange={setFormSchema}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormDesignerPage;
|
||||||
283
frontend/src/pages/Form/Definition/components/CreateModal.tsx
Normal file
283
frontend/src/pages/Form/Definition/components/CreateModal.tsx
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { FileText, Tag, Folder, AlignLeft, Copy, Loader2 } from 'lucide-react';
|
||||||
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
|
import { createDefinition } from '../service';
|
||||||
|
import { getEnabledCategories } from '../../Category/service';
|
||||||
|
import type { FormDefinitionRequest } from '../types';
|
||||||
|
import type { FormCategoryResponse } from '../../Category/types';
|
||||||
|
|
||||||
|
interface CreateModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess: (id: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义创建弹窗(第一步:输入基本信息)
|
||||||
|
*/
|
||||||
|
const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [categories, setCategories] = useState<FormCategoryResponse[]>([]);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
key: '',
|
||||||
|
categoryId: undefined as number | undefined,
|
||||||
|
description: '',
|
||||||
|
isTemplate: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载分类列表
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
loadCategories();
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
const loadCategories = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getEnabledCategories();
|
||||||
|
setCategories(result || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分类失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
key: '',
|
||||||
|
categoryId: undefined,
|
||||||
|
description: '',
|
||||||
|
isTemplate: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const handleClose = () => {
|
||||||
|
resetForm();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
// 验证
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
toast({
|
||||||
|
title: '验证失败',
|
||||||
|
description: '请输入表单名称',
|
||||||
|
variant: 'destructive'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.key.trim()) {
|
||||||
|
toast({
|
||||||
|
title: '验证失败',
|
||||||
|
description: '请输入表单标识',
|
||||||
|
variant: 'destructive'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证表单标识格式(英文字母、数字、中划线)
|
||||||
|
if (!/^[a-zA-Z0-9-]+$/.test(formData.key)) {
|
||||||
|
toast({
|
||||||
|
title: '验证失败',
|
||||||
|
description: '表单标识只能包含英文字母、数字和中划线',
|
||||||
|
variant: 'destructive'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
// 创建表单定义(只保存基本信息,schema 为空)
|
||||||
|
const request: FormDefinitionRequest = {
|
||||||
|
name: formData.name,
|
||||||
|
key: formData.key,
|
||||||
|
categoryId: formData.categoryId,
|
||||||
|
description: formData.description,
|
||||||
|
isTemplate: formData.isTemplate,
|
||||||
|
status: 'DRAFT',
|
||||||
|
schema: {
|
||||||
|
version: '1.0',
|
||||||
|
formConfig: {
|
||||||
|
labelAlign: 'right',
|
||||||
|
size: 'middle'
|
||||||
|
},
|
||||||
|
fields: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await createDefinition(request);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: '创建成功',
|
||||||
|
description: '表单基本信息已保存,现在可以开始设计表单'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 跳转到设计器页面
|
||||||
|
navigate(`/form/definitions/${result.id}/design`);
|
||||||
|
handleClose();
|
||||||
|
onSuccess(result.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建表单失败:', error);
|
||||||
|
toast({
|
||||||
|
title: '创建失败',
|
||||||
|
description: error instanceof Error ? error.message : '创建表单失败',
|
||||||
|
variant: 'destructive'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={visible} onOpenChange={handleClose}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>创建表单定义</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
第一步:输入表单的基本信息,然后点击"下一步"进入表单设计器
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 第一行:表单名称 + 表单标识 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="create-name" className="flex items-center gap-2">
|
||||||
|
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||||
|
表单名称
|
||||||
|
<span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="create-name"
|
||||||
|
placeholder="例如:员工请假申请表"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
className="h-10"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
将显示在表单列表和表单顶部
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="create-key" className="flex items-center gap-2">
|
||||||
|
<Tag className="h-4 w-4 text-muted-foreground" />
|
||||||
|
表单标识
|
||||||
|
<span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="create-key"
|
||||||
|
placeholder="例如:employee-leave-form"
|
||||||
|
value={formData.key}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, key: e.target.value }))}
|
||||||
|
className="h-10 font-mono"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
英文字母、数字和中划线,用于 API 调用
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 第二行:分类 + 设为模板 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="create-category" className="flex items-center gap-2">
|
||||||
|
<Folder className="h-4 w-4 text-muted-foreground" />
|
||||||
|
表单分类
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.categoryId?.toString() || undefined}
|
||||||
|
onValueChange={(value) => setFormData(prev => ({ ...prev, categoryId: Number(value) }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="create-category" className="h-10">
|
||||||
|
<SelectValue placeholder="选择表单所属分类" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{categories.map(cat => (
|
||||||
|
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Folder className="h-3.5 w-3.5" />
|
||||||
|
{cat.name}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
帮助用户快速找到相关表单
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="create-isTemplate" className="flex items-center gap-2">
|
||||||
|
<Copy className="h-4 w-4 text-muted-foreground" />
|
||||||
|
设为表单模板
|
||||||
|
</Label>
|
||||||
|
<div className="flex items-center h-10 rounded-lg border px-4 bg-muted/30">
|
||||||
|
<div className="flex-1">
|
||||||
|
<span className="text-sm">启用模板功能</span>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="create-isTemplate"
|
||||||
|
checked={formData.isTemplate}
|
||||||
|
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isTemplate: checked }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
模板表单可以被其他表单引用和复制
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 第三行:描述 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="create-description" className="flex items-center gap-2">
|
||||||
|
<AlignLeft className="h-4 w-4 text-muted-foreground" />
|
||||||
|
表单描述
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="create-description"
|
||||||
|
placeholder="简要说明此表单的用途和填写注意事项..."
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||||
|
className="min-h-[100px] resize-none"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
选填,用于帮助用户了解表单用途
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={handleClose} disabled={submitting}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={submitting}>
|
||||||
|
{submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||||
|
下一步:设计表单
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateModal;
|
||||||
|
|
||||||
@ -0,0 +1,229 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Loader2, FileText, Tag, Folder, AlignLeft, Copy } from 'lucide-react';
|
||||||
|
import { getEnabledCategories } from '../../Category/service';
|
||||||
|
import { updateDefinition } from '../service';
|
||||||
|
import type { FormCategoryResponse } from '../../Category/types';
|
||||||
|
import type { FormDefinitionRequest, FormDefinitionResponse } from '../types';
|
||||||
|
|
||||||
|
interface EditBasicInfoModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
record: FormDefinitionResponse | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditBasicInfoModal: React.FC<EditBasicInfoModalProps> = ({ visible, record, onClose, onSuccess }) => {
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [categories, setCategories] = useState<FormCategoryResponse[]>([]);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
key: '',
|
||||||
|
categoryId: undefined as number | undefined,
|
||||||
|
description: '',
|
||||||
|
isTemplate: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadCategories = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getEnabledCategories();
|
||||||
|
setCategories(result || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分类失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (visible) {
|
||||||
|
loadCategories();
|
||||||
|
if (record) {
|
||||||
|
setFormData({
|
||||||
|
name: record.name,
|
||||||
|
key: record.key,
|
||||||
|
categoryId: record.categoryId,
|
||||||
|
description: record.description || '',
|
||||||
|
isTemplate: record.isTemplate || false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [visible, record]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (!submitting) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!record) return;
|
||||||
|
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
alert('请输入表单名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.key.trim()) {
|
||||||
|
alert('请输入表单标识');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^[a-zA-Z0-9-]+$/.test(formData.key)) {
|
||||||
|
alert('表单标识只能包含英文字母、数字和中划线');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
const request: FormDefinitionRequest = {
|
||||||
|
name: formData.name,
|
||||||
|
key: formData.key,
|
||||||
|
categoryId: formData.categoryId,
|
||||||
|
description: formData.description,
|
||||||
|
schema: record.schema,
|
||||||
|
status: record.status,
|
||||||
|
isTemplate: formData.isTemplate,
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateDefinition(record.id, request);
|
||||||
|
alert('更新成功');
|
||||||
|
onSuccess();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新表单失败:', error);
|
||||||
|
alert('更新表单失败: ' + (error as Error).message);
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={visible} onOpenChange={handleClose}>
|
||||||
|
<DialogContent className="sm:max-w-[600px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>编辑基本信息</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
修改表单的名称、标识、分类等基本信息
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-6 py-4">
|
||||||
|
{/* 第一行:表单名称 + 表单标识 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-name" className="flex items-center gap-2">
|
||||||
|
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||||
|
表单名称 <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-name"
|
||||||
|
placeholder="例如:员工请假申请表"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
className="h-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-key" className="flex items-center gap-2">
|
||||||
|
<Tag className="h-4 w-4 text-muted-foreground" />
|
||||||
|
表单标识 <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-key"
|
||||||
|
placeholder="例如:employee-leave-form"
|
||||||
|
value={formData.key}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, key: e.target.value }))}
|
||||||
|
className="h-10 font-mono"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
英文字母、数字和中划线
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 第二行:分类 + 设为模板 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-category" className="flex items-center gap-2">
|
||||||
|
<Folder className="h-4 w-4 text-muted-foreground" />
|
||||||
|
表单分类
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.categoryId?.toString() || undefined}
|
||||||
|
onValueChange={(value) => setFormData(prev => ({ ...prev, categoryId: Number(value) }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="edit-category" className="h-10">
|
||||||
|
<SelectValue placeholder="选择表单所属分类" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{categories.map(cat => (
|
||||||
|
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Folder className="h-3.5 w-3.5" />
|
||||||
|
{cat.name}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-isTemplate" className="flex items-center gap-2">
|
||||||
|
<Copy className="h-4 w-4 text-muted-foreground" />
|
||||||
|
设为表单模板
|
||||||
|
</Label>
|
||||||
|
<div className="flex items-center h-10 rounded-lg border px-4 bg-muted/30">
|
||||||
|
<div className="flex-1">
|
||||||
|
<span className="text-sm">启用模板功能</span>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="edit-isTemplate"
|
||||||
|
checked={formData.isTemplate}
|
||||||
|
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isTemplate: checked }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 第三行:描述 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-description" className="flex items-center gap-2">
|
||||||
|
<AlignLeft className="h-4 w-4 text-muted-foreground" />
|
||||||
|
表单描述
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="edit-description"
|
||||||
|
placeholder="简要说明此表单的用途和填写注意事项..."
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||||
|
className="min-h-[100px] resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={handleClose} disabled={submitting}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={submitting}>
|
||||||
|
{submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||||
|
保存更改
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditBasicInfoModal;
|
||||||
|
|
||||||
613
frontend/src/pages/Form/Definition/index.tsx
Normal file
613
frontend/src/pages/Form/Definition/index.tsx
Normal file
@ -0,0 +1,613 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/card';
|
||||||
|
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog';
|
||||||
|
import { DataTablePagination } from '@/components/ui/pagination';
|
||||||
|
import { FormRenderer } from '@/components/FormDesigner';
|
||||||
|
import {
|
||||||
|
Loader2, Plus, Search, Eye, Edit, FileText, Ban, Trash2,
|
||||||
|
Database, MoreHorizontal, CheckCircle2, XCircle, Clock, AlertCircle,
|
||||||
|
Folder, Activity, Settings
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { getDefinitions, publishDefinition, disableDefinition, deleteDefinition } from './service';
|
||||||
|
import { getEnabledCategories } from '../Category/service';
|
||||||
|
import type { FormDefinitionResponse, FormDefinitionStatus } from './types';
|
||||||
|
import type { FormCategoryResponse } from '../Category/types';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||||
|
import CreateModal from './components/CreateModal';
|
||||||
|
import EditBasicInfoModal from './components/EditBasicInfoModal';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义列表页
|
||||||
|
*/
|
||||||
|
const FormDefinitionList: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState<Page<FormDefinitionResponse> | null>(null);
|
||||||
|
const [categories, setCategories] = useState<FormCategoryResponse[]>([]);
|
||||||
|
const [query, setQuery] = useState({
|
||||||
|
pageNum: DEFAULT_CURRENT - 1,
|
||||||
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
|
name: '',
|
||||||
|
categoryId: undefined as number | undefined,
|
||||||
|
status: undefined as FormDefinitionStatus | undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建表单弹窗
|
||||||
|
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||||
|
|
||||||
|
// 编辑基本信息弹窗
|
||||||
|
const [editBasicInfoVisible, setEditBasicInfoVisible] = useState(false);
|
||||||
|
const [editBasicInfoRecord, setEditBasicInfoRecord] = useState<FormDefinitionResponse | null>(null);
|
||||||
|
|
||||||
|
// 预览弹窗
|
||||||
|
const [previewVisible, setPreviewVisible] = useState(false);
|
||||||
|
const [previewForm, setPreviewForm] = useState<FormDefinitionResponse | null>(null);
|
||||||
|
|
||||||
|
// 删除确认弹窗
|
||||||
|
const [deleteVisible, setDeleteVisible] = useState(false);
|
||||||
|
const [deleteForm, setDeleteForm] = useState<FormDefinitionResponse | null>(null);
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
|
// 加载分类列表
|
||||||
|
const loadCategories = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getEnabledCategories();
|
||||||
|
setCategories(result || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分类失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await getDefinitions(query);
|
||||||
|
setData(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载表单定义失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadCategories();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
const total = data?.totalElements || 0;
|
||||||
|
const draftCount = data?.content?.filter(item => item.status === 'DRAFT').length || 0;
|
||||||
|
const publishedCount = data?.content?.filter(item => item.status === 'PUBLISHED').length || 0;
|
||||||
|
const disabledCount = data?.content?.filter(item => item.status === 'DISABLED').length || 0;
|
||||||
|
|
||||||
|
return { total, draftCount, publishedCount, disabledCount };
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
setQuery(prev => ({
|
||||||
|
...prev,
|
||||||
|
pageNum: 0,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const handleReset = () => {
|
||||||
|
setQuery({
|
||||||
|
pageNum: 0,
|
||||||
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
|
name: '',
|
||||||
|
categoryId: undefined,
|
||||||
|
status: undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建表单
|
||||||
|
const handleCreate = () => {
|
||||||
|
setCreateModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑表单设计
|
||||||
|
const handleEdit = (record: FormDefinitionResponse) => {
|
||||||
|
navigate(`/form/definitions/${record.id}/design`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑基本信息
|
||||||
|
const handleEditBasicInfo = (record: FormDefinitionResponse) => {
|
||||||
|
setEditBasicInfoRecord(record);
|
||||||
|
setEditBasicInfoVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 预览表单
|
||||||
|
const handlePreview = (record: FormDefinitionResponse) => {
|
||||||
|
setPreviewForm(record);
|
||||||
|
setPreviewVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发布表单
|
||||||
|
const handlePublish = async (record: FormDefinitionResponse) => {
|
||||||
|
try {
|
||||||
|
await publishDefinition(record.id);
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发布表单失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 禁用表单
|
||||||
|
const handleDisable = async (record: FormDefinitionResponse) => {
|
||||||
|
try {
|
||||||
|
await disableDefinition(record.id);
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('禁用表单失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开删除确认弹窗
|
||||||
|
const handleDeleteClick = (record: FormDefinitionResponse) => {
|
||||||
|
setDeleteForm(record);
|
||||||
|
setDeleteVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认删除
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (!deleteForm) return;
|
||||||
|
setDeleting(true);
|
||||||
|
try {
|
||||||
|
await deleteDefinition(deleteForm.id);
|
||||||
|
setDeleteVisible(false);
|
||||||
|
setDeleteForm(null);
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除表单失败:', error);
|
||||||
|
} finally {
|
||||||
|
setDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看数据
|
||||||
|
const handleViewData = (record: FormDefinitionResponse) => {
|
||||||
|
navigate(`/form/data?formDefinitionId=${record.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据分类 ID 获取分类信息
|
||||||
|
const getCategoryInfo = (categoryId?: number) => {
|
||||||
|
return categories.find(cat => cat.id === categoryId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 状态徽章(带图标)
|
||||||
|
const getStatusBadge = (status: FormDefinitionStatus) => {
|
||||||
|
const statusMap: Record<FormDefinitionStatus, {
|
||||||
|
variant: 'default' | 'secondary' | 'destructive' | 'success' | 'outline';
|
||||||
|
text: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
}> = {
|
||||||
|
DRAFT: {
|
||||||
|
variant: 'outline',
|
||||||
|
text: '草稿',
|
||||||
|
icon: <Clock className="h-3 w-3 mr-1" />
|
||||||
|
},
|
||||||
|
PUBLISHED: {
|
||||||
|
variant: 'success',
|
||||||
|
text: '已发布',
|
||||||
|
icon: <CheckCircle2 className="h-3 w-3 mr-1" />
|
||||||
|
},
|
||||||
|
DISABLED: {
|
||||||
|
variant: 'secondary',
|
||||||
|
text: '已禁用',
|
||||||
|
icon: <XCircle className="h-3 w-3 mr-1" />
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const statusInfo = statusMap[status];
|
||||||
|
return (
|
||||||
|
<Badge variant={statusInfo.variant} className="flex items-center w-fit">
|
||||||
|
{statusInfo.icon}
|
||||||
|
{statusInfo.text}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageCount = data?.totalElements ? Math.ceil(data.totalElements / query.pageSize) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6 p-6">
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">总表单数</CardTitle>
|
||||||
|
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.total}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
全部表单定义
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">草稿</CardTitle>
|
||||||
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.draftCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
待发布的表单
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">已发布</CardTitle>
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.publishedCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
正在使用的表单
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">已禁用</CardTitle>
|
||||||
|
<XCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.disabledCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
已停用的表单
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>表单定义管理</CardTitle>
|
||||||
|
<CardDescription className="mt-2">
|
||||||
|
创建和管理表单定义,支持版本控制和发布管理
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleCreate} size="default">
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
创建表单
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<Separator />
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
<div className="flex-1 max-w-md">
|
||||||
|
<Input
|
||||||
|
placeholder="搜索表单名称或标识..."
|
||||||
|
value={query.name}
|
||||||
|
onChange={(e) => setQuery(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={query.categoryId?.toString() || undefined}
|
||||||
|
onValueChange={(value) => setQuery(prev => ({ ...prev, categoryId: Number(value) }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[160px] h-9">
|
||||||
|
<SelectValue placeholder="全部分类" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{categories.map(cat => (
|
||||||
|
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{cat.icon && <Folder className="h-3.5 w-3.5" />}
|
||||||
|
{cat.name}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={query.status || undefined}
|
||||||
|
onValueChange={(value) => setQuery(prev => ({ ...prev, status: value as FormDefinitionStatus }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[140px] h-9">
|
||||||
|
<SelectValue placeholder="全部状态" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="DRAFT">草稿</SelectItem>
|
||||||
|
<SelectItem value="PUBLISHED">已发布</SelectItem>
|
||||||
|
<SelectItem value="DISABLED">已禁用</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button onClick={handleSearch} size="sm">
|
||||||
|
<Search className="h-4 w-4 mr-2" />
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleReset} size="sm">
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 表格 */}
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[250px]">表单名称</TableHead>
|
||||||
|
<TableHead className="w-[200px]">表单标识</TableHead>
|
||||||
|
<TableHead className="w-[150px]">分类</TableHead>
|
||||||
|
<TableHead className="w-[80px]">版本</TableHead>
|
||||||
|
<TableHead className="w-[120px]">状态</TableHead>
|
||||||
|
<TableHead className="w-[180px]">更新时间</TableHead>
|
||||||
|
<TableHead className="w-[100px] text-right">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="h-32 text-center">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||||
|
<span className="text-sm text-muted-foreground">加载中...</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : data?.content && data.content.length > 0 ? (
|
||||||
|
data.content.map((record) => {
|
||||||
|
const categoryInfo = getCategoryInfo(record.categoryId);
|
||||||
|
return (
|
||||||
|
<TableRow key={record.id} className="hover:bg-muted/50">
|
||||||
|
<TableCell>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{record.name}</div>
|
||||||
|
{record.description && (
|
||||||
|
<div className="text-xs text-muted-foreground mt-1 line-clamp-1">
|
||||||
|
{record.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<code className="text-xs bg-muted px-2 py-1 rounded">{record.key}</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{categoryInfo ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Folder className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm">{categoryInfo.name}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-muted-foreground">-</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline" className="font-mono">
|
||||||
|
v{record.formVersion}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{getStatusBadge(record.status)}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
|
{record.updateTime || record.createTime}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePreview(record)}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEdit(record)}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-[160px]">
|
||||||
|
<DropdownMenuItem onClick={() => handleEditBasicInfo(record)}>
|
||||||
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
|
编辑基本信息
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleViewData(record)}>
|
||||||
|
<Database className="h-4 w-4 mr-2" />
|
||||||
|
查看数据
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{record.status === 'DRAFT' && (
|
||||||
|
<DropdownMenuItem onClick={() => handlePublish(record)}>
|
||||||
|
<FileText className="h-4 w-4 mr-2" />
|
||||||
|
发布表单
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{record.status === 'PUBLISHED' && (
|
||||||
|
<DropdownMenuItem onClick={() => handleDisable(record)}>
|
||||||
|
<Ban className="h-4 w-4 mr-2" />
|
||||||
|
禁用表单
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDeleteClick(record)}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
删除表单
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="h-32 text-center">
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
|
<FileText className="w-12 h-12 mb-4 opacity-20" />
|
||||||
|
<div className="text-sm font-medium">暂无表单定义</div>
|
||||||
|
<div className="text-xs mt-1">点击右上角"创建表单"开始</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{pageCount > 1 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<DataTablePagination
|
||||||
|
pageIndex={query.pageNum + 1}
|
||||||
|
pageSize={query.pageSize}
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={(page) => setQuery(prev => ({
|
||||||
|
...prev,
|
||||||
|
pageNum: page - 1
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 预览弹窗 */}
|
||||||
|
{previewForm && (
|
||||||
|
<Dialog open={previewVisible} onOpenChange={setPreviewVisible}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>预览表单</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{previewForm.name} - {previewForm.key} (v{previewForm.formVersion})
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Separator className="my-4" />
|
||||||
|
<div className="py-4">
|
||||||
|
<FormRenderer
|
||||||
|
schema={previewForm.schema}
|
||||||
|
value={{}}
|
||||||
|
readonly={true}
|
||||||
|
showSubmit={false}
|
||||||
|
onCancel={() => setPreviewVisible(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 删除确认弹窗 */}
|
||||||
|
{deleteForm && (
|
||||||
|
<Dialog open={deleteVisible} onOpenChange={setDeleteVisible}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-destructive" />
|
||||||
|
确认删除
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
此操作不可撤销,确定要删除表单吗?
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<div className="rounded-lg bg-muted p-4 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">表单名称:</span>
|
||||||
|
<span className="text-sm font-medium">{deleteForm.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">表单标识:</span>
|
||||||
|
<code className="text-xs bg-background px-2 py-1 rounded">{deleteForm.key}</code>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">版本:</span>
|
||||||
|
<span className="text-sm">v{deleteForm.formVersion}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setDeleteVisible(false)}
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDeleteConfirm}
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
{deleting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 创建表单弹窗 */}
|
||||||
|
<CreateModal
|
||||||
|
visible={createModalVisible}
|
||||||
|
onClose={() => setCreateModalVisible(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setCreateModalVisible(false);
|
||||||
|
loadData();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 编辑基本信息弹窗 */}
|
||||||
|
<EditBasicInfoModal
|
||||||
|
visible={editBasicInfoVisible}
|
||||||
|
record={editBasicInfoRecord}
|
||||||
|
onClose={() => {
|
||||||
|
setEditBasicInfoVisible(false);
|
||||||
|
setEditBasicInfoRecord(null);
|
||||||
|
}}
|
||||||
|
onSuccess={() => {
|
||||||
|
loadData();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormDefinitionList;
|
||||||
58
frontend/src/pages/Form/Definition/service.ts
Normal file
58
frontend/src/pages/Form/Definition/service.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import type {
|
||||||
|
FormDefinitionQuery,
|
||||||
|
FormDefinitionResponse,
|
||||||
|
FormDefinitionRequest,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const BASE_URL = '/api/v1/forms/definitions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询表单定义列表
|
||||||
|
*/
|
||||||
|
export const getDefinitions = (params?: FormDefinitionQuery) =>
|
||||||
|
request.get<Page<FormDefinitionResponse>>(`${BASE_URL}/page`, { params });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表单定义详情
|
||||||
|
*/
|
||||||
|
export const getDefinitionById = (id: number) =>
|
||||||
|
request.get<FormDefinitionResponse>(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 key 获取表单定义(最新版本)
|
||||||
|
*/
|
||||||
|
export const getDefinitionByKey = (key: string) =>
|
||||||
|
request.get<FormDefinitionResponse>(`${BASE_URL}/by-key/${key}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建表单定义
|
||||||
|
*/
|
||||||
|
export const createDefinition = (data: FormDefinitionRequest) =>
|
||||||
|
request.post<FormDefinitionResponse>(BASE_URL, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新表单定义
|
||||||
|
*/
|
||||||
|
export const updateDefinition = (id: number, data: FormDefinitionRequest) =>
|
||||||
|
request.put<FormDefinitionResponse>(`${BASE_URL}/${id}`, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布表单
|
||||||
|
*/
|
||||||
|
export const publishDefinition = (id: number) =>
|
||||||
|
request.post<FormDefinitionResponse>(`${BASE_URL}/${id}/publish`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用表单
|
||||||
|
*/
|
||||||
|
export const disableDefinition = (id: number) =>
|
||||||
|
request.post<FormDefinitionResponse>(`${BASE_URL}/${id}/disable`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除表单定义
|
||||||
|
*/
|
||||||
|
export const deleteDefinition = (id: number) =>
|
||||||
|
request.delete(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
52
frontend/src/pages/Form/Definition/types.ts
Normal file
52
frontend/src/pages/Form/Definition/types.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { BaseQuery } from '@/types/base';
|
||||||
|
import type { FormSchema } from '@/components/FormDesigner';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义状态
|
||||||
|
*/
|
||||||
|
export type FormDefinitionStatus = 'DRAFT' | 'PUBLISHED' | 'DISABLED';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义查询参数
|
||||||
|
*/
|
||||||
|
export interface FormDefinitionQuery extends BaseQuery {
|
||||||
|
name?: string;
|
||||||
|
key?: string;
|
||||||
|
categoryId?: number; // 分类ID
|
||||||
|
status?: FormDefinitionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义响应
|
||||||
|
*/
|
||||||
|
export interface FormDefinitionResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
formVersion: number;
|
||||||
|
categoryId?: number; // 分类ID
|
||||||
|
description?: string;
|
||||||
|
schema: FormSchema;
|
||||||
|
tags?: string[];
|
||||||
|
status: FormDefinitionStatus;
|
||||||
|
isTemplate: boolean;
|
||||||
|
createBy?: string;
|
||||||
|
createTime?: string;
|
||||||
|
updateBy?: string;
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义创建/更新请求
|
||||||
|
*/
|
||||||
|
export interface FormDefinitionRequest {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
categoryId?: number; // 分类ID
|
||||||
|
description?: string;
|
||||||
|
schema: FormSchema;
|
||||||
|
tags?: string[];
|
||||||
|
status?: FormDefinitionStatus;
|
||||||
|
isTemplate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,7 +1,20 @@
|
|||||||
import React, {useEffect, useState} from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {Modal, Form, Input, message, Select} from 'antd';
|
import {
|
||||||
import type {WorkflowDefinition, WorkflowCategory} from '../types';
|
Dialog,
|
||||||
import {saveDefinition, updateDefinition, getWorkflowCategories} from '../service';
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Loader2, FileText, Tag, AlignLeft, Folder } from 'lucide-react';
|
||||||
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
|
import type { WorkflowDefinition, WorkflowCategoryResponse } from '../types';
|
||||||
|
import { saveDefinition, updateDefinition, getWorkflowCategoryList } from '../service';
|
||||||
|
|
||||||
interface EditModalProps {
|
interface EditModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -10,181 +23,262 @@ interface EditModalProps {
|
|||||||
record?: WorkflowDefinition;
|
record?: WorkflowDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditModal: React.FC<EditModalProps> = ({visible, onClose, onSuccess, record}) => {
|
const EditModal: React.FC<EditModalProps> = ({ visible, onClose, onSuccess, record }) => {
|
||||||
const [form] = Form.useForm();
|
const { toast } = useToast();
|
||||||
const isEdit = !!record;
|
const isEdit = !!record;
|
||||||
const [categories, setCategories] = useState<WorkflowCategory[]>([]);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [selectedCategory, setSelectedCategory] = useState<WorkflowCategory>();
|
const [categories, setCategories] = useState<WorkflowCategoryResponse[]>([]);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
key: '',
|
||||||
|
categoryId: undefined as number | undefined,
|
||||||
|
description: '',
|
||||||
|
triggers: [] as string[],
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
loadCategories();
|
loadCategories();
|
||||||
|
if (record) {
|
||||||
|
setFormData({
|
||||||
|
name: record.name,
|
||||||
|
key: record.key,
|
||||||
|
categoryId: record.categoryId,
|
||||||
|
description: record.description || '',
|
||||||
|
triggers: record.triggers || [],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
key: '',
|
||||||
|
categoryId: undefined,
|
||||||
|
description: '',
|
||||||
|
triggers: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [visible]);
|
}, [visible, record]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible && record) {
|
|
||||||
// 找到当前分类
|
|
||||||
const category = categories.find(c => c.code === record.category);
|
|
||||||
setSelectedCategory(category);
|
|
||||||
|
|
||||||
// 设置表单值,使用lable显示
|
|
||||||
form.setFieldsValue({
|
|
||||||
...record,
|
|
||||||
// 分类显示lable
|
|
||||||
category: {
|
|
||||||
label: category?.lable,
|
|
||||||
value: record.category
|
|
||||||
},
|
|
||||||
// 触发器显示lable
|
|
||||||
triggers: record.triggers?.map(triggerCode => ({
|
|
||||||
label: category?.supportedTriggers.find(t => t.code === triggerCode)?.lable,
|
|
||||||
value: triggerCode
|
|
||||||
})) || []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [visible, record, categories]);
|
|
||||||
|
|
||||||
const loadCategories = async () => {
|
const loadCategories = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getWorkflowCategories();
|
const data = await getWorkflowCategoryList();
|
||||||
setCategories(data);
|
setCategories(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载工作流分类失败:', error);
|
console.error('加载工作流分类失败:', error);
|
||||||
message.error('加载工作流分类失败');
|
toast({
|
||||||
|
title: '加载失败',
|
||||||
|
description: '加载工作流分类失败',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCategoryChange = (selected: { value: string, label: string }) => {
|
const handleClose = () => {
|
||||||
const category = categories.find(c => c.code === selected.value);
|
if (!submitting) {
|
||||||
setSelectedCategory(category);
|
onClose();
|
||||||
// 当切换分类时,清空触发器选择
|
}
|
||||||
form.setFieldValue('triggers', []);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
// 验证
|
||||||
const values = await form.validateFields();
|
if (!formData.name.trim()) {
|
||||||
const submitData = {
|
toast({
|
||||||
...values,
|
title: '验证失败',
|
||||||
// 提取code值提交给后端
|
description: '请输入流程名称',
|
||||||
category: values.category.value,
|
variant: 'destructive',
|
||||||
triggers: values.triggers.map((t: {value: string}) => t.value),
|
});
|
||||||
flowVersion: isEdit ? record.flowVersion : 1,
|
return;
|
||||||
status: isEdit ? record.status : 'DRAFT'
|
}
|
||||||
};
|
if (!formData.key.trim()) {
|
||||||
|
toast({
|
||||||
|
title: '验证失败',
|
||||||
|
description: '请输入流程标识',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(formData.key)) {
|
||||||
|
toast({
|
||||||
|
title: '验证失败',
|
||||||
|
description: '流程标识只能包含字母、数字、下划线和连字符,且必须以字母或下划线开头',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (/^xml/i.test(formData.key)) {
|
||||||
|
toast({
|
||||||
|
title: '验证失败',
|
||||||
|
description: '流程标识不能以xml开头',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.categoryId) {
|
||||||
|
toast({
|
||||||
|
title: '验证失败',
|
||||||
|
description: '请选择流程分类',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isEdit) {
|
setSubmitting(true);
|
||||||
await updateDefinition(record.id, {
|
try {
|
||||||
...record,
|
const submitData: WorkflowDefinition = {
|
||||||
...submitData,
|
...formData,
|
||||||
|
id: record?.id || 0,
|
||||||
|
flowVersion: isEdit ? record.flowVersion : 1,
|
||||||
|
status: isEdit ? record.status : 'DRAFT',
|
||||||
|
graph: record?.graph || { nodes: [], edges: [] },
|
||||||
|
formConfig: record?.formConfig || { formItems: [] },
|
||||||
|
} as WorkflowDefinition;
|
||||||
|
|
||||||
|
if (isEdit && record) {
|
||||||
|
await updateDefinition(record.id, submitData);
|
||||||
|
toast({
|
||||||
|
title: '更新成功',
|
||||||
|
description: `工作流 "${formData.name}" 已更新`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await saveDefinition(submitData as WorkflowDefinition);
|
await saveDefinition(submitData);
|
||||||
|
toast({
|
||||||
|
title: '创建成功',
|
||||||
|
description: `工作流 "${formData.name}" 已创建`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
message.success(isEdit ? '更新成功' : '保存成功');
|
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
onClose();
|
onClose();
|
||||||
form.resetFields();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
message.error(error.message);
|
toast({
|
||||||
|
title: isEdit ? '更新失败' : '创建失败',
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectedCategory = categories.find(c => c.id === formData.categoryId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Dialog open={visible} onOpenChange={handleClose}>
|
||||||
title={isEdit ? '编辑流程' : '新建流程'}
|
<DialogContent className="sm:max-w-[600px]">
|
||||||
open={visible}
|
<DialogHeader>
|
||||||
onOk={handleOk}
|
<DialogTitle>{isEdit ? '编辑流程' : '新建流程'}</DialogTitle>
|
||||||
onCancel={() => {
|
</DialogHeader>
|
||||||
onClose();
|
<div className="grid gap-6 py-4">
|
||||||
form.resetFields();
|
{/* 流程分类 */}
|
||||||
}}
|
<div className="space-y-2">
|
||||||
destroyOnClose
|
<Label htmlFor="categoryId" className="flex items-center gap-2">
|
||||||
>
|
<Folder className="h-4 w-4 text-muted-foreground" />
|
||||||
<Form
|
流程分类 <span className="text-destructive">*</span>
|
||||||
form={form}
|
</Label>
|
||||||
layout="vertical"
|
<Select
|
||||||
preserve={false}
|
value={formData.categoryId?.toString() || undefined}
|
||||||
>
|
onValueChange={(value) => {
|
||||||
<Form.Item
|
setFormData(prev => ({
|
||||||
name="category"
|
...prev,
|
||||||
label="流程分类"
|
categoryId: Number(value),
|
||||||
rules={[{required: true, message: '请选择流程分类'}]}
|
triggers: [], // 切换分类时清空触发器
|
||||||
>
|
}));
|
||||||
<Select
|
}}
|
||||||
placeholder="请选择流程分类"
|
disabled={isEdit}
|
||||||
onChange={handleCategoryChange}
|
>
|
||||||
disabled={isEdit}
|
<SelectTrigger id="categoryId" className="h-10">
|
||||||
labelInValue
|
<SelectValue placeholder="请选择流程分类" />
|
||||||
>
|
</SelectTrigger>
|
||||||
{(categories || []).map(category => (
|
<SelectContent>
|
||||||
<Select.Option key={category.code} value={category.code}>
|
{categories.map(cat => (
|
||||||
{category.lable}
|
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||||||
</Select.Option>
|
{cat.name}
|
||||||
))}
|
</SelectItem>
|
||||||
</Select>
|
))}
|
||||||
</Form.Item>
|
</SelectContent>
|
||||||
<Form.Item
|
</Select>
|
||||||
name="triggers"
|
{isEdit && (
|
||||||
label="触发方式"
|
<p className="text-xs text-muted-foreground">
|
||||||
rules={[{required: true, message: '请选择触发方式'}]}
|
编辑时不可修改分类
|
||||||
>
|
</p>
|
||||||
<Select
|
)}
|
||||||
mode="multiple"
|
</div>
|
||||||
placeholder="请选择触发方式"
|
|
||||||
disabled={!selectedCategory || isEdit}
|
{/* 流程名称 */}
|
||||||
labelInValue
|
<div className="space-y-2">
|
||||||
>
|
<Label htmlFor="name" className="flex items-center gap-2">
|
||||||
{(selectedCategory?.supportedTriggers || []).map(trigger => (
|
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||||
<Select.Option key={trigger.code} value={trigger.code}>
|
流程名称 <span className="text-destructive">*</span>
|
||||||
{trigger.lable}
|
</Label>
|
||||||
</Select.Option>
|
<Input
|
||||||
))}
|
id="name"
|
||||||
</Select>
|
placeholder="例如:Jenkins 构建流程"
|
||||||
</Form.Item>
|
value={formData.name}
|
||||||
<Form.Item
|
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||||
name="name"
|
className="h-10"
|
||||||
label="流程名称"
|
/>
|
||||||
rules={[{required: true, message: '请输入流程名称'}]}
|
</div>
|
||||||
>
|
|
||||||
<Input placeholder="请输入流程名称"/>
|
{/* 流程标识 */}
|
||||||
</Form.Item>
|
<div className="space-y-2">
|
||||||
<Form.Item
|
<Label htmlFor="key" className="flex items-center gap-2">
|
||||||
name="key"
|
<Tag className="h-4 w-4 text-muted-foreground" />
|
||||||
label="流程标识"
|
流程标识 <span className="text-destructive">*</span>
|
||||||
rules={[
|
{isEdit && <span className="text-xs text-muted-foreground">(不可修改)</span>}
|
||||||
{ required: true, message: '请输入流程标识' },
|
</Label>
|
||||||
{
|
<Input
|
||||||
pattern: /^[a-zA-Z_][a-zA-Z0-9_-]*$/,
|
id="key"
|
||||||
message: '流程标识只能包含字母、数字、下划线(_)和连字符(-),且必须以字母或下划线开头'
|
placeholder="例如:jenkins_build_workflow"
|
||||||
},
|
value={formData.key}
|
||||||
{
|
onChange={(e) => setFormData(prev => ({ ...prev, key: e.target.value }))}
|
||||||
pattern: /^(?!xml)/i,
|
disabled={isEdit}
|
||||||
message: '流程标识不能以xml开头(不区分大小写)'
|
className="h-10 font-mono"
|
||||||
},
|
/>
|
||||||
{
|
<p className="text-xs text-muted-foreground">
|
||||||
pattern: /^[^.\s]*$/,
|
只能包含字母、数字、下划线和连字符,建议使用小写字母和下划线
|
||||||
message: '流程标识不能包含空格和点号'
|
</p>
|
||||||
},
|
</div>
|
||||||
{
|
|
||||||
max: 64,
|
{/* 描述 */}
|
||||||
message: '流程标识长度不能超过64个字符'
|
<div className="space-y-2">
|
||||||
}
|
<Label htmlFor="description" className="flex items-center gap-2">
|
||||||
]}
|
<AlignLeft className="h-4 w-4 text-muted-foreground" />
|
||||||
>
|
流程描述
|
||||||
<Input placeholder="请输入流程标识,建议使用小写字母和下划线" disabled={isEdit}/>
|
</Label>
|
||||||
</Form.Item>
|
<Textarea
|
||||||
<Form.Item
|
id="description"
|
||||||
name="description"
|
placeholder="简要说明此工作流的用途..."
|
||||||
label="描述"
|
value={formData.description}
|
||||||
>
|
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||||
<Input.TextArea placeholder="请输入流程描述"/>
|
className="min-h-[100px] resize-none"
|
||||||
</Form.Item>
|
/>
|
||||||
</Form>
|
</div>
|
||||||
</Modal>
|
|
||||||
|
{/* 提示:触发方式在设计阶段配置 */}
|
||||||
|
{selectedCategory && selectedCategory.supportedTriggers && selectedCategory.supportedTriggers.length > 0 && (
|
||||||
|
<div className="rounded-lg border p-4 bg-muted/30">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<strong>支持的触发方式:</strong> {selectedCategory.supportedTriggers.join(', ')}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
创建后可在工作流设计页面配置具体的触发条件
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={handleClose} disabled={submitting}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={submitting}>
|
||||||
|
{submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||||
|
{isEdit ? '更新' : '创建'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,33 @@
|
|||||||
import React, {useState, useEffect} from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import {Table, Card, Button, Space, Tag, message, Modal} from 'antd';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {PlusOutlined, ExclamationCircleOutlined} from '@ant-design/icons';
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||||
import {useNavigate} from 'react-router-dom';
|
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog';
|
||||||
|
import { DataTablePagination } from '@/components/ui/pagination';
|
||||||
|
import {
|
||||||
|
Loader2, Plus, Search, Edit, Trash2, Play, CheckCircle2,
|
||||||
|
Clock, Activity, Workflow, MoreHorizontal, AlertCircle, Eye
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import * as service from './service';
|
import * as service from './service';
|
||||||
import type {WorkflowDefinition, WorkflowDefinitionQuery, WorkflowCategory} from './types';
|
import type { WorkflowDefinition, WorkflowDefinitionQuery, WorkflowCategoryResponse } from './types';
|
||||||
import {DEFAULT_PAGE_SIZE, DEFAULT_CURRENT} from '@/utils/page';
|
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||||
import EditModal from './components/EditModal';
|
import EditModal from './components/EditModal';
|
||||||
|
|
||||||
const {confirm} = Modal;
|
|
||||||
|
|
||||||
const WorkflowDefinitionList: React.FC = () => {
|
const WorkflowDefinitionList: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [pageData, setPageData] = useState<{
|
const [pageData, setPageData] = useState<{
|
||||||
content: WorkflowDefinition[];
|
content: WorkflowDefinition[];
|
||||||
@ -20,10 +37,17 @@ const WorkflowDefinitionList: React.FC = () => {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [currentRecord, setCurrentRecord] = useState<WorkflowDefinition>();
|
const [currentRecord, setCurrentRecord] = useState<WorkflowDefinition>();
|
||||||
const [categories, setCategories] = useState<WorkflowCategory[]>([]);
|
const [categories, setCategories] = useState<WorkflowCategoryResponse[]>([]);
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
const [deleteRecord, setDeleteRecord] = useState<WorkflowDefinition | null>(null);
|
||||||
|
const [deployDialogOpen, setDeployDialogOpen] = useState(false);
|
||||||
|
const [deployRecord, setDeployRecord] = useState<WorkflowDefinition | null>(null);
|
||||||
const [query, setQuery] = useState<WorkflowDefinitionQuery>({
|
const [query, setQuery] = useState<WorkflowDefinitionQuery>({
|
||||||
pageNum: DEFAULT_CURRENT - 1,
|
pageNum: DEFAULT_CURRENT - 1,
|
||||||
pageSize: DEFAULT_PAGE_SIZE
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
|
name: '',
|
||||||
|
categoryId: undefined,
|
||||||
|
status: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadData = async (params: WorkflowDefinitionQuery) => {
|
const loadData = async (params: WorkflowDefinitionQuery) => {
|
||||||
@ -33,7 +57,11 @@ const WorkflowDefinitionList: React.FC = () => {
|
|||||||
setPageData(data);
|
setPageData(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
message.error(error.message);
|
toast({
|
||||||
|
title: '加载失败',
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -42,17 +70,19 @@ const WorkflowDefinitionList: React.FC = () => {
|
|||||||
|
|
||||||
const loadCategories = async () => {
|
const loadCategories = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await service.getWorkflowCategories();
|
const data = await service.getWorkflowCategoryList();
|
||||||
setCategories(data);
|
setCategories(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载工作流分类失败:', error);
|
console.error('加载工作流分类失败:', error);
|
||||||
message.error('加载工作流分类失败');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData(query);
|
|
||||||
loadCategories();
|
loadCategories();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData(query);
|
||||||
}, [query]);
|
}, [query]);
|
||||||
|
|
||||||
const handleCreateFlow = () => {
|
const handleCreateFlow = () => {
|
||||||
@ -74,169 +104,414 @@ const WorkflowDefinitionList: React.FC = () => {
|
|||||||
setCurrentRecord(undefined);
|
setCurrentRecord(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeploy = async (id: number) => {
|
const handleSearch = () => {
|
||||||
confirm({
|
setQuery(prev => ({
|
||||||
title: '确认发布',
|
...prev,
|
||||||
icon: <ExclamationCircleOutlined/>,
|
pageNum: 0,
|
||||||
content: '确定要发布该流程定义吗?发布后将不能修改。',
|
}));
|
||||||
onOk: async () => {
|
};
|
||||||
try {
|
|
||||||
await service.publishDefinition(id);
|
const handleReset = () => {
|
||||||
message.success('发布成功');
|
setQuery({
|
||||||
loadData(query);
|
pageNum: 0,
|
||||||
} catch (error) {
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
if (error instanceof Error) {
|
name: '',
|
||||||
message.error(error.message);
|
categoryId: undefined,
|
||||||
}
|
status: undefined
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
const handleDeploy = (record: WorkflowDefinition) => {
|
||||||
confirm({
|
setDeployRecord(record);
|
||||||
title: '确认删除',
|
setDeployDialogOpen(true);
|
||||||
icon: <ExclamationCircleOutlined/>,
|
|
||||||
content: '确定要删除该流程定义吗?删除后不可恢复。',
|
|
||||||
onOk: async () => {
|
|
||||||
try {
|
|
||||||
await service.deleteDefinition(id);
|
|
||||||
message.success('删除成功');
|
|
||||||
loadData(query);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
message.error(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStartFlow = async (record: WorkflowDefinition) => {
|
const confirmDeploy = async () => {
|
||||||
|
if (!deployRecord) return;
|
||||||
try {
|
try {
|
||||||
await service.startWorkflowInstance(record.key, record.category);
|
await service.publishDefinition(deployRecord.id);
|
||||||
message.success('流程启动成功');
|
toast({
|
||||||
} catch (error) {
|
title: '发布成功',
|
||||||
if (error instanceof Error) {
|
description: `工作流 "${deployRecord.name}" 已发布`,
|
||||||
message.error(error.message);
|
});
|
||||||
|
loadData(query);
|
||||||
|
setDeployDialogOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast({
|
||||||
|
title: '发布失败',
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const handleDelete = (record: WorkflowDefinition) => {
|
||||||
{
|
setDeleteRecord(record);
|
||||||
title: '流程名称',
|
setDeleteDialogOpen(true);
|
||||||
dataIndex: 'name',
|
};
|
||||||
key: 'name',
|
|
||||||
},
|
const confirmDelete = async () => {
|
||||||
{
|
if (!deleteRecord) return;
|
||||||
title: '流程标识',
|
try {
|
||||||
dataIndex: 'key',
|
await service.deleteDefinition(deleteRecord.id);
|
||||||
key: 'key',
|
toast({
|
||||||
},
|
title: '删除成功',
|
||||||
{
|
description: `工作流 "${deleteRecord.name}" 已删除`,
|
||||||
title: '流程分类',
|
});
|
||||||
dataIndex: 'category',
|
loadData(query);
|
||||||
key: 'category',
|
setDeleteDialogOpen(false);
|
||||||
render: (category: string) => {
|
} catch (error) {
|
||||||
const categoryInfo = categories.find(c => c.code === category);
|
if (error instanceof Error) {
|
||||||
return categoryInfo?.label || category;
|
toast({
|
||||||
}
|
title: '删除失败',
|
||||||
},
|
description: error.message,
|
||||||
{
|
variant: 'destructive'
|
||||||
title: '触发方式',
|
|
||||||
dataIndex: 'triggers',
|
|
||||||
key: 'triggers',
|
|
||||||
render: (triggers: string[], record: WorkflowDefinition) => {
|
|
||||||
const categoryInfo = categories.find(c => c.code === record.category);
|
|
||||||
return (triggers || [])?.map(triggerCode => {
|
|
||||||
const triggerInfo = categoryInfo?.supportedTriggers?.find(t => t.code === triggerCode);
|
|
||||||
return (
|
|
||||||
<Tag key={triggerCode}>
|
|
||||||
{triggerInfo?.lable || triggerCode}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{
|
};
|
||||||
title: '版本',
|
|
||||||
dataIndex: 'flowVersion',
|
const handleStartFlow = async (record: WorkflowDefinition) => {
|
||||||
key: 'flowVersion',
|
try {
|
||||||
},
|
await service.startWorkflowInstance(record.key, record.categoryId);
|
||||||
{
|
toast({
|
||||||
title: '状态',
|
title: '启动成功',
|
||||||
dataIndex: 'status',
|
description: `工作流 "${record.name}" 已启动`,
|
||||||
key: 'status',
|
});
|
||||||
render: (status: string) => (
|
} catch (error) {
|
||||||
<Tag color={status === 'DRAFT' ? 'orange' : 'green'}>
|
if (error instanceof Error) {
|
||||||
{status === 'DRAFT' ? '草稿' : '已发布'}
|
toast({
|
||||||
</Tag>
|
title: '启动失败',
|
||||||
),
|
description: error.message,
|
||||||
},
|
variant: 'destructive'
|
||||||
{
|
});
|
||||||
title: '描述',
|
}
|
||||||
dataIndex: 'description',
|
}
|
||||||
key: 'description',
|
};
|
||||||
ellipsis: true,
|
|
||||||
},
|
// 状态徽章
|
||||||
{
|
const getStatusBadge = (status: string) => {
|
||||||
title: '操作',
|
const statusMap: Record<string, {
|
||||||
key: 'action',
|
variant: 'default' | 'secondary' | 'destructive' | 'success' | 'outline';
|
||||||
fixed: 'right',
|
text: string;
|
||||||
width: 200,
|
icon: React.ElementType
|
||||||
render: (_: any, record: WorkflowDefinition) => (
|
}> = {
|
||||||
<Space size="middle">
|
DRAFT: { variant: 'outline', text: '草稿', icon: Clock },
|
||||||
{record.status === 'DRAFT' && (
|
PUBLISHED: { variant: 'success', text: '已发布', icon: CheckCircle2 },
|
||||||
<>
|
};
|
||||||
<a onClick={() => handleEditFlow(record)}>编辑</a>
|
const statusInfo = statusMap[status] || { variant: 'outline', text: status, icon: Clock };
|
||||||
<a onClick={() => handleDesignFlow(record)}>设计</a>
|
const Icon = statusInfo.icon;
|
||||||
</>
|
return (
|
||||||
)}
|
<Badge variant={statusInfo.variant} className="flex items-center gap-1">
|
||||||
{record.status === 'DRAFT' && (
|
<Icon className="h-3 w-3" />
|
||||||
<a onClick={() => handleDeploy(record.id)}>发布</a>
|
{statusInfo.text}
|
||||||
)}
|
</Badge>
|
||||||
<a onClick={() => handleDelete(record.id)}>删除</a>
|
);
|
||||||
{record.status !== 'DRAFT' && (
|
};
|
||||||
<a onClick={() => handleStartFlow(record)}>启动</a>
|
|
||||||
)}
|
// 统计数据
|
||||||
</Space>
|
const stats = useMemo(() => {
|
||||||
),
|
const total = pageData?.totalElements || 0;
|
||||||
},
|
const draftCount = pageData?.content?.filter(d => d.status === 'DRAFT').length || 0;
|
||||||
];
|
const publishedCount = pageData?.content?.filter(d => d.status !== 'DRAFT').length || 0;
|
||||||
|
return { total, draftCount, publishedCount };
|
||||||
|
}, [pageData]);
|
||||||
|
|
||||||
|
const pageCount = pageData?.totalElements ? Math.ceil(pageData.totalElements / (query.pageSize || DEFAULT_PAGE_SIZE)) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div className="p-6">
|
||||||
title={
|
<div className="mb-6">
|
||||||
<Button type="primary" icon={<PlusOutlined/>} onClick={handleCreateFlow}>
|
<h1 className="text-3xl font-bold text-foreground">工作流定义管理</h1>
|
||||||
新建流程
|
</div>
|
||||||
</Button>
|
|
||||||
}
|
{/* 统计卡片 */}
|
||||||
>
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||||
<Table
|
<Card className="bg-gradient-to-br from-blue-500/10 to-blue-500/5 border-blue-500/20">
|
||||||
columns={columns}
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
dataSource={pageData?.content}
|
<CardTitle className="text-sm font-medium text-blue-700">总工作流</CardTitle>
|
||||||
loading={loading}
|
<Activity className="h-4 w-4 text-blue-500" />
|
||||||
rowKey="id"
|
</CardHeader>
|
||||||
scroll={{ x: 1300 }}
|
<CardContent>
|
||||||
pagination={{
|
<div className="text-2xl font-bold">{stats.total}</div>
|
||||||
current: (query.pageNum || 0) + 1,
|
<p className="text-xs text-muted-foreground mt-1">全部工作流定义</p>
|
||||||
pageSize: query.pageSize,
|
</CardContent>
|
||||||
total: pageData?.totalElements || 0,
|
</Card>
|
||||||
onChange: (page, pageSize) => setQuery({
|
<Card className="bg-gradient-to-br from-yellow-500/10 to-yellow-500/5 border-yellow-500/20">
|
||||||
...query,
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
pageNum: page - 1,
|
<CardTitle className="text-sm font-medium text-yellow-700">草稿</CardTitle>
|
||||||
pageSize
|
<Clock className="h-4 w-4 text-yellow-500" />
|
||||||
}),
|
</CardHeader>
|
||||||
}}
|
<CardContent>
|
||||||
/>
|
<div className="text-2xl font-bold">{stats.draftCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">待发布的工作流</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-gradient-to-br from-green-500/10 to-green-500/5 border-green-500/20">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-green-700">已发布</CardTitle>
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.publishedCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">正在使用的工作流</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>工作流列表</CardTitle>
|
||||||
|
<Button onClick={handleCreateFlow}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
新建工作流
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<div className="flex flex-wrap items-center gap-4 mb-4">
|
||||||
|
<div className="flex-1 max-w-md">
|
||||||
|
<Input
|
||||||
|
placeholder="搜索工作流名称"
|
||||||
|
value={query.name}
|
||||||
|
onChange={(e) => setQuery(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={query.categoryId?.toString() || undefined}
|
||||||
|
onValueChange={(value) => setQuery(prev => ({ ...prev, categoryId: Number(value) }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[160px] h-9">
|
||||||
|
<SelectValue placeholder="全部分类" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{categories.map(cat => (
|
||||||
|
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||||||
|
{cat.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={query.status || undefined}
|
||||||
|
onValueChange={(value) => setQuery(prev => ({ ...prev, status: value }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[140px] h-9">
|
||||||
|
<SelectValue placeholder="全部状态" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="DRAFT">草稿</SelectItem>
|
||||||
|
<SelectItem value="PUBLISHED">已发布</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button onClick={handleSearch} className="h-9">
|
||||||
|
<Search className="h-4 w-4 mr-2" />
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleReset} className="h-9">
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 表格 */}
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[200px]">流程名称</TableHead>
|
||||||
|
<TableHead className="w-[150px]">流程标识</TableHead>
|
||||||
|
<TableHead className="w-[120px]">流程分类</TableHead>
|
||||||
|
<TableHead className="w-[80px]">版本</TableHead>
|
||||||
|
<TableHead className="w-[100px]">状态</TableHead>
|
||||||
|
<TableHead>描述</TableHead>
|
||||||
|
<TableHead className="text-right">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="h-24 text-center">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||||
|
<span className="text-sm text-muted-foreground">加载中...</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : pageData?.content && pageData.content.length > 0 ? (
|
||||||
|
pageData.content.map((record) => {
|
||||||
|
const categoryInfo = categories.find(c => c.id === record.categoryId);
|
||||||
|
return (
|
||||||
|
<TableRow key={record.id} className="hover:bg-muted/50">
|
||||||
|
<TableCell className="font-medium">{record.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">
|
||||||
|
{record.key}
|
||||||
|
</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{categoryInfo ? (
|
||||||
|
<Badge variant="outline">
|
||||||
|
{categoryInfo.name}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline">未分类</Badge>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="font-mono text-sm">{record.flowVersion || 1}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{getStatusBadge(record.status || 'DRAFT')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm line-clamp-1">{record.description || '-'}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">打开菜单</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{record.status === 'DRAFT' && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem onClick={() => handleEditFlow(record)}>
|
||||||
|
<Edit className="h-4 w-4 mr-2" /> 编辑
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleDesignFlow(record)}>
|
||||||
|
<Workflow className="h-4 w-4 mr-2" /> 设计
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleDeploy(record)}>
|
||||||
|
<CheckCircle2 className="h-4 w-4 mr-2" /> 发布
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{record.status !== 'DRAFT' && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem onClick={() => handleStartFlow(record)}>
|
||||||
|
<Play className="h-4 w-4 mr-2" /> 启动
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleDesignFlow(record)}>
|
||||||
|
<Eye className="h-4 w-4 mr-2" /> 查看
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDelete(record)}
|
||||||
|
className="text-red-600 focus:text-red-600"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" /> 删除
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="h-24 text-center">
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
|
<Workflow className="w-16 h-16 mb-4 text-muted-foreground/50" />
|
||||||
|
<div className="text-lg font-semibold mb-2">暂无工作流定义</div>
|
||||||
|
<div className="text-sm">点击右上角"新建工作流"开始设计您的第一个工作流。</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{pageCount > 1 && (
|
||||||
|
<DataTablePagination
|
||||||
|
pageIndex={(query.pageNum || 0) + 1}
|
||||||
|
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={(page) => setQuery(prev => ({
|
||||||
|
...prev,
|
||||||
|
pageNum: page - 1
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* EditModal */}
|
||||||
<EditModal
|
<EditModal
|
||||||
visible={modalVisible}
|
visible={modalVisible}
|
||||||
onClose={handleModalClose}
|
onClose={handleModalClose}
|
||||||
onSuccess={() => loadData(query)}
|
onSuccess={() => loadData(query)}
|
||||||
record={currentRecord}
|
record={currentRecord}
|
||||||
/>
|
/>
|
||||||
</Card>
|
|
||||||
|
{/* 发布确认对话框 */}
|
||||||
|
{deployRecord && (
|
||||||
|
<Dialog open={deployDialogOpen} onOpenChange={setDeployDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2 text-green-600">
|
||||||
|
<CheckCircle2 className="h-5 w-5" /> 确认发布工作流?
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
您确定要发布工作流 "<span className="font-semibold text-foreground">{deployRecord.name}</span>" 吗?
|
||||||
|
发布后将可以启动执行。
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setDeployDialogOpen(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={confirmDeploy}>
|
||||||
|
确认发布
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 删除确认对话框 */}
|
||||||
|
{deleteRecord && (
|
||||||
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2 text-red-600">
|
||||||
|
<AlertCircle className="h-5 w-5" /> 确认删除工作流?
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
您确定要删除工作流 "<span className="font-semibold text-foreground">{deleteRecord.name}</span>" 吗?
|
||||||
|
此操作不可逆。
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">标识:</span> <code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">{deleteRecord.key}</code>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">版本:</span> <span className="font-mono text-sm">{deleteRecord.flowVersion || 1}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">状态:</span> {getStatusBadge(deleteRecord.status || 'DRAFT')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={confirmDelete}>
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import {WorkflowDefinition, WorkflowDefinitionQuery} from './types';
|
import {
|
||||||
|
WorkflowDefinition,
|
||||||
|
WorkflowDefinitionQuery,
|
||||||
|
WorkflowCategoryResponse,
|
||||||
|
WorkflowCategoryQuery,
|
||||||
|
WorkflowCategoryRequest
|
||||||
|
} from './types';
|
||||||
import {Page} from '@/types/base';
|
import {Page} from '@/types/base';
|
||||||
import {WorkflowCategory} from './types'; // Add this line
|
|
||||||
|
|
||||||
const DEFINITION_URL = '/api/v1/workflow/definition';
|
const DEFINITION_URL = '/api/v1/workflow/definition';
|
||||||
const INSTANCE_URL = '/api/v1/workflow/instance';
|
const INSTANCE_URL = '/api/v1/workflow/instance';
|
||||||
|
const CATEGORY_URL = '/api/v1/workflow/categories';
|
||||||
|
|
||||||
export const getDefinitions = (params?: WorkflowDefinitionQuery) =>
|
export const getDefinitions = (params?: WorkflowDefinitionQuery) =>
|
||||||
request.get<Page<WorkflowDefinition>>(`${DEFINITION_URL}/page`, {params});
|
request.get<Page<WorkflowDefinition>>(`${DEFINITION_URL}/page`, {params});
|
||||||
@ -36,21 +42,62 @@ export const publishDefinition = (id: number) =>
|
|||||||
request.post<void>(`${DEFINITION_URL}/${id}/published`);
|
request.post<void>(`${DEFINITION_URL}/${id}/published`);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取工作流分类列表
|
* 分页查询工作流分类
|
||||||
* @returns Promise<WorkflowCategory[]> 工作流分类列表
|
* @param params 查询参数
|
||||||
|
* @returns Promise<Page<WorkflowCategoryResponse>>
|
||||||
*/
|
*/
|
||||||
export const getWorkflowCategories = () =>
|
export const getWorkflowCategories = (params?: WorkflowCategoryQuery) =>
|
||||||
request.get<WorkflowCategory[]>(`${DEFINITION_URL}/categories`);
|
request.get<Page<WorkflowCategoryResponse>>(`${CATEGORY_URL}/page`, {params});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询工作流分类列表
|
||||||
|
* @returns Promise<WorkflowCategoryResponse[]>
|
||||||
|
*/
|
||||||
|
export const getWorkflowCategoryList = () =>
|
||||||
|
request.get<WorkflowCategoryResponse[]>(`${CATEGORY_URL}/list`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个工作流分类
|
||||||
|
* @param id 分类ID
|
||||||
|
* @returns Promise<WorkflowCategoryResponse>
|
||||||
|
*/
|
||||||
|
export const getWorkflowCategoryById = (id: number) =>
|
||||||
|
request.get<WorkflowCategoryResponse>(`${CATEGORY_URL}/${id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建工作流分类
|
||||||
|
* @param data 分类数据
|
||||||
|
* @returns Promise<WorkflowCategoryResponse>
|
||||||
|
*/
|
||||||
|
export const createWorkflowCategory = (data: WorkflowCategoryRequest) =>
|
||||||
|
request.post<WorkflowCategoryResponse>(CATEGORY_URL, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新工作流分类
|
||||||
|
* @param id 分类ID
|
||||||
|
* @param data 分类数据
|
||||||
|
* @returns Promise<WorkflowCategoryResponse>
|
||||||
|
*/
|
||||||
|
export const updateWorkflowCategory = (id: number, data: WorkflowCategoryRequest) =>
|
||||||
|
request.put<WorkflowCategoryResponse>(`${CATEGORY_URL}/${id}`, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除工作流分类
|
||||||
|
* @param id 分类ID
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
export const deleteWorkflowCategory = (id: number) =>
|
||||||
|
request.delete<void>(`${CATEGORY_URL}/${id}`);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动工作流实例
|
* 启动工作流实例
|
||||||
* @param processKey 流程定义key
|
* @param processKey 流程定义key
|
||||||
* @param categoryCode 分类编码
|
* @param categoryId 分类ID
|
||||||
* @returns Promise<void>
|
* @returns Promise<void>
|
||||||
*/
|
*/
|
||||||
export const startWorkflowInstance = (processKey: string, categoryCode: string) =>
|
export const startWorkflowInstance = (processKey: string, categoryId?: number) =>
|
||||||
request.post<void>(`${INSTANCE_URL}/start`, {
|
request.post<void>(`${INSTANCE_URL}/start`, {
|
||||||
processKey,
|
processKey,
|
||||||
businessKey: `${categoryCode}_${Date.now()}`,
|
businessKey: `workflow_${Date.now()}`,
|
||||||
|
categoryId
|
||||||
});
|
});
|
||||||
@ -7,7 +7,7 @@ export interface WorkflowDefinition extends BaseResponse {
|
|||||||
description?: string;
|
description?: string;
|
||||||
flowVersion?: number;
|
flowVersion?: number;
|
||||||
status?: string;
|
status?: string;
|
||||||
category: string;
|
categoryId?: number; // 分类ID
|
||||||
triggers: string[];
|
triggers: string[];
|
||||||
graph: {
|
graph: {
|
||||||
nodes: WorkflowDefinitionNode[];
|
nodes: WorkflowDefinitionNode[];
|
||||||
@ -60,23 +60,46 @@ export interface WorkflowDefinitionNode {
|
|||||||
export interface WorkflowDefinitionQuery extends BaseQuery {
|
export interface WorkflowDefinitionQuery extends BaseQuery {
|
||||||
name?: string;
|
name?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
|
categoryId?: number; // 分类ID筛选
|
||||||
status?: string;
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流触发器类型
|
* 工作流分类响应
|
||||||
*/
|
*/
|
||||||
export interface WorkflowTrigger {
|
export interface WorkflowCategoryResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
label: string;
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
sort: number;
|
||||||
|
supportedTriggers?: string[]; // 触发方式代码列表,如 ["MANUAL","SCHEDULED"]
|
||||||
|
enabled: boolean;
|
||||||
|
createBy?: string;
|
||||||
|
createTime?: string;
|
||||||
|
updateBy?: string;
|
||||||
|
updateTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流分类
|
* 工作流分类查询参数
|
||||||
*/
|
*/
|
||||||
export interface WorkflowCategory {
|
export interface WorkflowCategoryQuery extends BaseQuery {
|
||||||
code: string;
|
name?: string;
|
||||||
label: string;
|
code?: string;
|
||||||
description: string;
|
enabled?: boolean;
|
||||||
supportedTriggers: WorkflowTrigger[];
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流分类创建/更新请求
|
||||||
|
*/
|
||||||
|
export interface WorkflowCategoryRequest {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
sort?: number;
|
||||||
|
supportedTriggers?: string[];
|
||||||
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -220,7 +220,7 @@ const DetailModal: React.FC<DetailModalProps> = ({ visible, onCancel, instanceDa
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{instanceData.graph ? (
|
{instanceData.graph ? (
|
||||||
<Card>
|
<Card>
|
||||||
@ -272,9 +272,9 @@ const DetailModal: React.FC<DetailModalProps> = ({ visible, onCancel, instanceDa
|
|||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="text-center py-10 text-muted-foreground">
|
<div className="text-center py-10 text-muted-foreground">
|
||||||
暂无流程图数据
|
暂无流程图数据
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@ -308,7 +308,7 @@ const DetailModal: React.FC<DetailModalProps> = ({ visible, onCancel, instanceDa
|
|||||||
<span className="text-xs text-muted-foreground font-normal">
|
<span className="text-xs text-muted-foreground font-normal">
|
||||||
{getNodeTypeText(stage.nodeType)}
|
{getNodeTypeText(stage.nodeType)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground mb-2">
|
<div className="text-sm text-muted-foreground mb-2">
|
||||||
{stage.startTime && dayjs(stage.startTime).format('YYYY-MM-DD HH:mm:ss')}
|
{stage.startTime && dayjs(stage.startTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
{stage.endTime && ` → ${dayjs(stage.endTime).format('HH:mm:ss')}`}
|
{stage.endTime && ` → ${dayjs(stage.endTime).format('HH:mm:ss')}`}
|
||||||
@ -322,7 +322,7 @@ const DetailModal: React.FC<DetailModalProps> = ({ visible, onCancel, instanceDa
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@ -353,7 +353,7 @@ const DetailModal: React.FC<DetailModalProps> = ({ visible, onCancel, instanceDa
|
|||||||
<DescriptionItem label="执行节点数" value={instanceData.stages.length} />
|
<DescriptionItem label="执行节点数" value={instanceData.stages.length} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
|
|||||||
@ -129,7 +129,7 @@ const HistoryModal: React.FC<HistoryModalProps> = ({ visible, onCancel, workflow
|
|||||||
pageSize={query.pageSize}
|
pageSize={query.pageSize}
|
||||||
pageCount={pageCount}
|
pageCount={pageCount}
|
||||||
onPageChange={(page) => setQuery(prev => ({
|
onPageChange={(page) => setQuery(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
pageNum: page - 1
|
pageNum: page - 1
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -45,6 +45,10 @@ const JenkinsManagerList = lazy(() => import('../pages/Deploy/JenkinsManager/Lis
|
|||||||
const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
|
const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
|
||||||
const External = lazy(() => import('../pages/Deploy/External'));
|
const External = lazy(() => import('../pages/Deploy/External'));
|
||||||
const FormDesigner = lazy(() => import('../pages/FormDesigner'));
|
const FormDesigner = lazy(() => import('../pages/FormDesigner'));
|
||||||
|
const FormDefinitionList = lazy(() => import('../pages/Form/Definition'));
|
||||||
|
const FormDefinitionDesigner = lazy(() => import('../pages/Form/Definition/Designer'));
|
||||||
|
const FormDataList = lazy(() => import('../pages/Form/Data'));
|
||||||
|
const FormDataDetail = lazy(() => import('../pages/Form/Data/Detail'));
|
||||||
|
|
||||||
// Workflow2 相关路由已迁移到 Workflow,删除旧路由
|
// Workflow2 相关路由已迁移到 Workflow,删除旧路由
|
||||||
|
|
||||||
@ -148,6 +152,51 @@ const router = createBrowserRouter([
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'form',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'definitions',
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<LoadingComponent/>}>
|
||||||
|
<FormDefinitionList/>
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'definitions/create',
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<LoadingComponent/>}>
|
||||||
|
<FormDefinitionDesigner/>
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'definitions/:id/design',
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<LoadingComponent/>}>
|
||||||
|
<FormDefinitionDesigner/>
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'data',
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<LoadingComponent/>}>
|
||||||
|
<FormDataList/>
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'data/:id',
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<LoadingComponent/>}>
|
||||||
|
<FormDataDetail/>
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'workflow',
|
path: 'workflow',
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user