refactor: 重构为多模块

This commit is contained in:
Ray.Hao
2026-04-25 16:17:08 +08:00
parent 63c34a4218
commit 482990cba1
351 changed files with 953 additions and 1088 deletions

41
youlai-codegen/pom.xml Normal file
View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.youlai</groupId>
<artifactId>youlai-boot</artifactId>
<version>4.3.0</version>
</parent>
<artifactId>youlai-codegen</artifactId>
<description>代码生成器模块 - 可视化配置的前后端代码生成</description>
<dependencies>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>youlai-common</artifactId>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>youlai-framework</artifactId>
</dependency>
<!-- Velocity 模板引擎 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>
<!-- MyBatis-Plus 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus-generator.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,103 @@
package com.youlai.boot.codegen.config;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.map.MapUtil;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* 代码生成配置属性
*
* @author Ray
* @since 2.11.0
*/
@Component
@EnableConfigurationProperties(CodegenProperties.class)
@ConfigurationProperties(prefix = "codegen")
@Data
public class CodegenProperties {
/**
* 默认配置
*/
private DefaultConfig defaultConfig ;
/**
* 模板配置
*/
private Map<String, TemplateConfig> templateConfigs = MapUtil.newHashMap(true);
/**
* 后端应用名
*/
private String backendAppName;
/**
* 前端应用名
*/
private String frontendAppName;
/**
* 下载文件名
*/
private String downloadFileName;
/**
* 排除数据表
*/
private List<String> excludeTables;
/**
* 模板配置
*/
@Data
public static class TemplateConfig {
/**
* 模板路径 (e.g. /templates/codegen/controller.java.vm)
*/
private String templatePath;
/**
* 子包名 (e.g. controller/service/mapper/model)
*/
private String subpackageName;
/**
* 文件扩展名,如 .java
*/
private String extension = FileNameUtil.EXT_JAVA;
}
/**
* 默认配置
*/
@Data
public static class DefaultConfig {
/**
* 作者 (e.g. Ray)
*/
private String author;
/**
* 默认模块名(e.g. system)
*/
private String moduleName;
/**
* 基础包名 (e.g. com.youlai.boot)
*/
private String packageName = "com.youlai.boot";
}
}

View File

@@ -0,0 +1,112 @@
package com.youlai.boot.codegen.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.common.result.PageResult;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.codegen.config.CodegenProperties;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.codegen.service.CodegenService;
import com.youlai.boot.codegen.model.form.GenConfigForm;
import com.youlai.boot.codegen.model.query.TableQuery;
import com.youlai.boot.codegen.model.vo.CodegenPreviewVO;
import com.youlai.boot.codegen.model.vo.TablePageVO;
import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.codegen.service.GenTableService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* 代码生成器控制层
*
* @author Ray
* @since 2.10.0
*/
@Tag(name = "11.代码生成")
@RestController
@RequestMapping("/api/v1/codegen")
@RequiredArgsConstructor
@Slf4j
public class CodegenController {
private final CodegenService codegenService;
private final GenTableService genTableService;
private final CodegenProperties codegenProperties;
@Operation(summary = "获取数据表分页列表")
@GetMapping("/table")
public PageResult<TablePageVO> getTablePage(
TableQuery queryParams
) {
Page<TablePageVO> result = codegenService.getTablePage(queryParams);
return PageResult.success(result);
}
@Operation(summary = "获取代码生成配置")
@GetMapping("/{tableName}/config")
public Result<GenConfigForm> getGenTableFormData(
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
) {
GenConfigForm formData = genTableService.getGenTableFormData(tableName);
return Result.success(formData);
}
@Operation(summary = "保存代码生成配置")
@PostMapping("/{tableName}/config")
@Log(module = LogModuleEnum.CODEGEN, value = ActionTypeEnum.UPDATE)
public Result<?> saveGenConfig(@RequestBody GenConfigForm formData) {
genTableService.saveGenConfig(formData);
return Result.success();
}
@Operation(summary = "删除代码生成配置")
@DeleteMapping("/{tableName}/config")
public Result<?> deleteGenConfig(
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
) {
genTableService.deleteGenConfig(tableName);
return Result.success();
}
@Operation(summary = "获取预览生成代码")
@GetMapping("/{tableName}/preview")
public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName,
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType,
@RequestParam(value = "type", required = false, defaultValue = "ts") String type) {
List<CodegenPreviewVO> list = codegenService.getCodegenPreviewData(tableName, pageType, type);
return Result.success(list);
}
@Operation(summary = "下载代码")
@GetMapping("/{tableName}/download")
@Log(module = LogModuleEnum.CODEGEN, value = ActionTypeEnum.DOWNLOAD)
public void downloadZip(HttpServletResponse response, @PathVariable String tableName,
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType,
@RequestParam(value = "type", required = false, defaultValue = "ts") String type) {
String[] tableNames = tableName.split(",");
byte[] data = codegenService.downloadCode(tableNames, pageType, type);
response.reset();
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8));
response.setContentType("application/octet-stream; charset=UTF-8");
try (ServletOutputStream outputStream = response.getOutputStream()) {
outputStream.write(data);
outputStream.flush();
} catch (IOException e) {
log.error("Error while writing the zip file1 to response", e);
throw new RuntimeException("Failed to write the zip file1 to response", e);
}
}
}

View File

@@ -0,0 +1,41 @@
package com.youlai.boot.codegen.converter;
import com.youlai.boot.codegen.model.entity.GenTable;
import com.youlai.boot.codegen.model.entity.GenTableColumn;
import com.youlai.boot.codegen.model.form.GenConfigForm;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.util.List;
/**
* 代码生成配置转换器
*
* @author Ray
* @since 2.10.0
*/
@Mapper(componentModel = "spring")
public interface CodegenConverter {
@Mapping(source = "genTable.tableName", target = "tableName")
@Mapping(source = "genTable.businessName", target = "businessName")
@Mapping(source = "genTable.moduleName", target = "moduleName")
@Mapping(source = "genTable.packageName", target = "packageName")
@Mapping(source = "genTable.entityName", target = "entityName")
@Mapping(source = "genTable.author", target = "author")
@Mapping(source = "genTable.pageType", target = "pageType")
@Mapping(source = "genTable.removeTablePrefix", target = "removeTablePrefix")
@Mapping(source = "fieldConfigs", target = "fieldConfigs")
GenConfigForm toGenConfigForm(GenTable genTable, List<GenTableColumn> fieldConfigs);
List<GenConfigForm.FieldConfig> toGenTableColumnForm(List<GenTableColumn> fieldConfigs);
GenConfigForm.FieldConfig toGenTableColumnForm(GenTableColumn genTableColumn);
GenTable toGenTable(GenConfigForm formData);
List<GenTableColumn> toGenTableColumn(List<GenConfigForm.FieldConfig> fieldConfigs);
GenTableColumn toGenTableColumn(GenConfigForm.FieldConfig fieldConfig);
}

View File

@@ -0,0 +1,89 @@
package com.youlai.boot.codegen.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.youlai.boot.common.base.IBaseEnum;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 表单类型枚举
*
* @author Ray
* @since 2.10.0
*/
@Getter
@RequiredArgsConstructor
public enum FormTypeEnum implements IBaseEnum<Integer> {
/**
* 输入框
*/
INPUT(1, "输入框"),
/**
* 下拉框
*/
SELECT(2, "下拉框"),
/**
* 单选框
*/
RADIO(3, "单选框"),
/**
* 复选框
*/
CHECK_BOX(4, "复选框"),
/**
* 数字输入框
*/
INPUT_NUMBER(5, "数字输入框"),
/**
* 开关
*/
SWITCH(6, "开关"),
/**
* 文本域
*/
TEXT_AREA(7, "文本域"),
/**
* 日期时间框
*/
DATE(8, "日期框"),
/**
* 日期框
*/
DATE_TIME(9, "日期时间框"),
/**
* 隐藏域
*/
HIDDEN(10, "隐藏域");
// Mybatis-Plus 提供注解表示插入数据库时插入该值
@EnumValue
@JsonValue
private final Integer value;
// @JsonValue // 表示对枚举序列化时返回此字段
private final String label;
@JsonCreator
public static FormTypeEnum fromValue(Integer value) {
for (FormTypeEnum type : FormTypeEnum.values()) {
if (type.getValue().equals(value)) {
return type;
}
}
throw new IllegalArgumentException("No enum constant with value " + value);
}
}

View File

@@ -0,0 +1,108 @@
package com.youlai.boot.codegen.enums;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
/**
* 表单类型枚举
*
* @author Ray
* @since 2.10.0
*/
@Getter
public enum JavaTypeEnum {
VARCHAR("varchar", "String", "string"),
CHAR("char", "String", "string"),
BLOB("blob", "byte[]", "Uint8Array"),
TEXT("text", "String", "string"),
JSON("json", "String", "any"),
INTEGER("int", "Integer", "number"),
TINYINT("tinyint", "Integer", "number"),
SMALLINT("smallint", "Integer", "number"),
MEDIUMINT("mediumint", "Integer", "number"),
BIGINT("bigint", "Long", "number"),
FLOAT("float", "Float", "number"),
DOUBLE("double", "Double", "number"),
DECIMAL("decimal", "BigDecimal", "number"),
DATE("date", "LocalDate", "string"),
DATETIME("datetime", "LocalDateTime", "string"),
TIMESTAMP("timestamp", "LocalDateTime", "string"),
BOOLEAN("boolean", "Boolean", "boolean"),
BIT("bit", "Boolean", "boolean");
// 数据库类型
private final String dbType;
// Java类型
private final String javaType;
// TypeScript类型
private final String tsType;
// 数据库类型和Java类型的映射
private static final Map<String, JavaTypeEnum> typeMap = new HashMap<>();
// 初始化映射关系
static {
for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) {
typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum);
}
}
JavaTypeEnum(String dbType, String javaType, String tsType) {
this.dbType = dbType;
this.javaType = javaType;
this.tsType = tsType;
}
/**
* 根据数据库类型获取对应的Java类型
*
* @param columnType 列类型
* @return 对应的Java类型
*/
public static String getJavaTypeByColumnType(String columnType) {
String normalized = normalizeColumnType(columnType);
JavaTypeEnum javaTypeEnum = typeMap.get(normalized);
if (javaTypeEnum != null) {
return javaTypeEnum.getJavaType();
}
return "String";
}
/**
* 根据Java类型获取对应的TypeScript类型
*
* @param javaType Java类型
* @return 对应的TypeScript类型
*/
public static String getTsTypeByJavaType(String javaType) {
if (javaType == null) {
return "any";
}
for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) {
if (javaTypeEnum.getJavaType().equals(javaType)) {
return javaTypeEnum.getTsType();
}
}
return "any";
}
private static String normalizeColumnType(String columnType) {
if (columnType == null) {
return "";
}
// Handle values like: varchar(255), bigint unsigned, INT
String normalized = columnType.trim().toLowerCase();
int parenIndex = normalized.indexOf('(');
if (parenIndex > -1) {
normalized = normalized.substring(0, parenIndex);
}
// Remove modifiers
normalized = normalized.replace("unsigned", "").replace("zerofill", "").trim();
// Collapse repeated spaces
normalized = normalized.replaceAll("\\s+", " ");
return normalized;
}
}

View File

@@ -0,0 +1,73 @@
package com.youlai.boot.codegen.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.youlai.boot.common.base.IBaseEnum;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 查询类型枚举
*
* @author Ray
* @since 2.10.0
*/
@Getter
@RequiredArgsConstructor
public enum QueryTypeEnum implements IBaseEnum<Integer> {
/** 等于 */
EQ(1, "="),
/** 模糊匹配 */
LIKE(2, "LIKE '%s%'"),
/** 包含 */
IN(3, "IN"),
/** 范围 */
BETWEEN(4, "BETWEEN"),
/** 大于 */
GT(5, ">"),
/** 大于等于 */
GE(6, ">="),
/** 小于 */
LT(7, "<"),
/** 小于等于 */
LE(8, "<="),
/** 不等于 */
NE(9, "!="),
/** 左模糊匹配 */
LIKE_LEFT(10, "LIKE '%s'"),
/** 右模糊匹配 */
LIKE_RIGHT(11, "LIKE 's%'");
// 存储在数据库中的枚举属性值
@EnumValue
@JsonValue
private final Integer value;
// 序列化成 JSON 时的属性值
private final String label;
@JsonCreator
public static QueryTypeEnum fromValue(Integer value) {
for (QueryTypeEnum type : QueryTypeEnum.values()) {
if (type.getValue().equals(value)) {
return type;
}
}
throw new IllegalArgumentException("No enum constant with value " + value);
}
}

View File

@@ -0,0 +1,41 @@
package com.youlai.boot.codegen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.codegen.model.vo.ColumnMetaVO;
import com.youlai.boot.codegen.model.vo.TableMetaVO;
import com.youlai.boot.codegen.model.query.TableQuery;
import com.youlai.boot.codegen.model.vo.TablePageVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 数据库映射层
*
* @author Ray
* @since 2.9.0
*/
@Mapper
public interface DatabaseMapper extends BaseMapper {
/**
* 获取表分页列表
*
* @param page
* @param queryParams
* @return
*/
Page<TablePageVO> getTablePage(Page<TablePageVO> page, TableQuery queryParams);
/**
* 获取表字段列表
*
* @param tableName
* @return
*/
List<ColumnMetaVO> getTableColumns(String tableName);
TableMetaVO getTableMetadata(String tableName);
}

View File

@@ -0,0 +1,20 @@
package com.youlai.boot.codegen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.boot.codegen.model.entity.GenTableColumn;
import org.apache.ibatis.annotations.Mapper;
/**
* 代码生成表字段配置访问层
*
* @author Ray
* @since 2.10.0
*/
@Mapper
public interface GenTableColumnMapper extends BaseMapper<GenTableColumn> {
}

View File

@@ -0,0 +1,20 @@
package com.youlai.boot.codegen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.boot.codegen.model.entity.GenTable;
import org.apache.ibatis.annotations.Mapper;
/**
* 代码生成表配置访问层
*
* @author Ray
* @since 2.10.0
*/
@Mapper
public interface GenTableMapper extends BaseMapper<GenTable> {
}

View File

@@ -0,0 +1,65 @@
package com.youlai.boot.codegen.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.youlai.boot.common.base.BaseEntity;
import lombok.Getter;
import lombok.Setter;
/**
* 代码生成表配置
*
* @author Ray
* @since 2.10.0
*/
@TableName(value = "gen_table")
@Getter
@Setter
public class GenTable extends BaseEntity {
/**
* 表名
*/
private String tableName;
/**
* 包名
*/
private String packageName;
/**
* 模块名
*/
private String moduleName;
/**
* 实体类名
*/
private String entityName;
/**
* 业务名
*/
private String businessName;
/**
* 父菜单ID
*/
private Long parentMenuId;
/**
* 作者
*/
private String author;
/**
* 页面类型 classic|curd
*/
private String pageType;
/**
* 要移除的表前缀,如: sys_
*/
private String removeTablePrefix;
}

View File

@@ -0,0 +1,107 @@
package com.youlai.boot.codegen.model.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.youlai.boot.common.base.BaseEntity;
import com.youlai.boot.codegen.enums.FormTypeEnum;
import com.youlai.boot.codegen.enums.QueryTypeEnum;
import lombok.Getter;
import lombok.Setter;
/**
* 代码生成表字段配置实体
*
* @author Ray
* @since 2.10.0
*/
@TableName(value = "gen_table_column")
@Getter
@Setter
public class GenTableColumn extends BaseEntity {
/**
* 关联的表配置ID
*/
private Long tableId;
/**
* 列名
*/
private String columnName;
/**
* 列类型
*/
private String columnType;
/**
* 字段长度
*/
private Long maxLength;
/**
* 字段名称
*/
private String fieldName;
/**
* 字段排序
*/
private Integer fieldSort;
/**
* 字段类型
*/
private String fieldType;
/**
* 字段描述
*/
private String fieldComment;
/**
* 表单类型
*/
private FormTypeEnum formType;
/**
* 查询方式
*/
private QueryTypeEnum queryType;
/**
* 是否在列表显示
*/
private Integer isShowInList;
/**
* 是否在表单显示
*/
private Integer isShowInForm;
/**
* 是否在查询条件显示
*/
private Integer isShowInQuery;
/**
* 是否必填
*/
private Integer isRequired;
/**
* TypeScript类型
*/
@TableField(exist = false)
@JsonIgnore
private String tsType;
/**
* 字典类型
*/
private String dictType;
}

View File

@@ -0,0 +1,109 @@
package com.youlai.boot.codegen.model.form;
import com.youlai.boot.codegen.enums.FormTypeEnum;
import com.youlai.boot.codegen.enums.QueryTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 代码生成配置表单
*
* @author Ray
* @since 2.10.0
*/
@Schema(description = "代码生成配置表单")
@Data
public class GenConfigForm {
@Schema(description = "主键",example = "1")
private Long id;
@Schema(description = "表名",example = "sys_user")
private String tableName;
@Schema(description = "业务名",example = "用户")
private String businessName;
@Schema(description = "模块名",example = "system")
private String moduleName;
@Schema(description = "包名",example = "com.youlai")
private String packageName;
@Schema(description = "实体名",example = "User")
private String entityName;
@Schema(description = "作者",example = "youlaitech")
private String author;
@Schema(description = "上级菜单ID",example = "1")
private Long parentMenuId;
@Schema(description = "字段配置列表")
private List<FieldConfig> fieldConfigs;
@Schema(description = "后端应用名")
private String backendAppName;
@Schema(description = "前端应用名")
private String frontendAppName;
@Schema(description = "页面类型 classic|curd", example = "classic")
private String pageType;
@Schema(description = "要移除的表前缀,如: sys_", example = "sys_")
private String removeTablePrefix;
@Schema(description = "字段配置")
@Data
public static class FieldConfig {
@Schema(description = "主键")
private Long id;
@Schema(description = "列名")
private String columnName;
@Schema(description = "列类型")
private String columnType;
@Schema(description = "字段名")
private String fieldName;
@Schema(description = "字段排序")
private Integer fieldSort;
@Schema(description = "字段类型")
private String fieldType;
@Schema(description = "字段描述")
private String fieldComment;
@Schema(description = "是否在列表显示")
private Integer isShowInList;
@Schema(description = "是否在表单显示")
private Integer isShowInForm;
@Schema(description = "是否在查询条件显示")
private Integer isShowInQuery;
@Schema(description = "是否必填")
private Integer isRequired;
@Schema(description = "最大长度")
private Integer maxLength;
@Schema(description = "表单类型")
private FormTypeEnum formType;
@Schema(description = "查询类型")
private QueryTypeEnum queryType;
@Schema(description = "字典类型")
private String dictType;
}
}

View File

@@ -0,0 +1,31 @@
package com.youlai.boot.codegen.model.query;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.youlai.boot.common.base.BaseQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* 数据表分页查询对象
*
* @author Ray
* @since 2.10.0
*/
@Schema(description = "数据表分页查询对象")
@Getter
@Setter
public class TablePageQuery extends BaseQuery {
@Schema(description="关键字(表名)")
private String keywords;
/**
* 排除的表名
*/
@JsonIgnore
private List<String> excludeTables;
}

View File

@@ -0,0 +1,31 @@
package com.youlai.boot.codegen.model.query;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.youlai.boot.common.base.BaseQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* 数据表查询对象
*
* @author Ray
* @since 2.10.0
*/
@Schema(description = "数据表查询对象")
@Getter
@Setter
public class TableQuery extends BaseQuery {
@Schema(description="关键字(表名)")
private String keywords;
/**
* 排除的表名
*/
@JsonIgnore
private List<String> excludeTables;
}

View File

@@ -0,0 +1,25 @@
package com.youlai.boot.codegen.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "代码生成代码预览Vo")
@Data
public class CodegenPreviewVO {
@Schema(description = "生成文件路径")
private String path;
@Schema(description = "生成文件名称",example = "SysUser.java" )
private String fileName;
@Schema(description = "生成文件内容")
private String content;
@Schema(description = "文件范围(frontend/backend)")
private String scope;
@Schema(description = "文件语言(扩展名)")
private String language;
}

View File

@@ -0,0 +1,26 @@
package com.youlai.boot.codegen.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "数据表字段元数据")
@Data
public class ColumnMetaVO {
private String columnName;
private String dataType;
private String columnComment;
private Long characterMaximumLength;
private Integer isPrimaryKey;
private String isNullable;
private String characterSetName;
private String collationName;
}

View File

@@ -0,0 +1,26 @@
package com.youlai.boot.codegen.model.vo;
import lombok.Data;
/**
* 数据表元数据
*
* @author Ray
* @since 2.10.0
*/
@Data
public class TableMetaVO {
private String tableName;
private String tableComment;
private String tableCollation;
private String engine;
private String charset;
private String createTime;
}

View File

@@ -0,0 +1,32 @@
package com.youlai.boot.codegen.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "表视图对象")
@Data
public class TablePageVO {
@Schema(description = "表名称", example = "sys_user")
private String tableName;
@Schema(description = "表描述",example = "用户表")
private String tableComment;
@Schema(description = "表排序规则",example = "utf8mb4_general_ci")
private String tableCollation;
@Schema(description = "存储引擎",example = "InnoDB")
private String engine;
@Schema(description = "字符集",example = "utf8mb4")
private String charset;
@Schema(description = "创建时间",example = "2023-08-08 08:08:08")
private String createTime;
@Schema(description="是否已配置")
private Integer isConfigured;
}

View File

@@ -0,0 +1,40 @@
package com.youlai.boot.codegen.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.codegen.model.query.TableQuery;
import com.youlai.boot.codegen.model.vo.CodegenPreviewVO;
import com.youlai.boot.codegen.model.vo.TablePageVO;
import java.util.List;
/**
* 代码生成配置接口
*
* @author Ray
* @since 2.10.0
*/
public interface CodegenService {
/**
* 获取数据表分页列表
*
* @param queryParams 查询参数
* @return
*/
Page<TablePageVO> getTablePage(TableQuery queryParams);
/**
* 获取预览生成代码
*
* @param tableName 表名
* @return
*/
List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType, String type);
/**
* 下载代码
* @param tableNames 表名
* @return
*/
byte[] downloadCode(String[] tableNames, String pageType, String type);
}

View File

@@ -0,0 +1,14 @@
package com.youlai.boot.codegen.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.boot.codegen.model.entity.GenTableColumn;
/**
* 代码生成配置接口
*
* @author Ray
* @since 2.10.0
*/
public interface GenTableColumnService extends IService<GenTableColumn> {
}

View File

@@ -0,0 +1,39 @@
package com.youlai.boot.codegen.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.boot.codegen.model.entity.GenTable;
import com.youlai.boot.codegen.model.form.GenConfigForm;
/**
* 代码生成配置接口
*
* @author Ray
* @since 2.10.0
*/
public interface GenTableService extends IService<GenTable> {
/**
* 获取代码生成配置
*
* @param tableName 表名
* @return
*/
GenConfigForm getGenTableFormData(String tableName);
/**
* 保存代码生成配置
*
* @param formData 表单数据
* @return
*/
void saveGenConfig(GenConfigForm formData);
/**
* 删除代码生成配置
*
* @param tableName 表名
* @return
*/
void deleteGenConfig(String tableName);
}

View File

@@ -0,0 +1,428 @@
package com.youlai.boot.codegen.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.template.Template;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.codegen.enums.JavaTypeEnum;
import com.youlai.boot.codegen.config.CodegenProperties;
import com.youlai.boot.codegen.service.GenTableService;
import com.youlai.boot.codegen.service.GenTableColumnService;
import com.youlai.boot.codegen.service.CodegenService;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.codegen.mapper.DatabaseMapper;
import com.youlai.boot.codegen.model.entity.GenTable;
import com.youlai.boot.codegen.model.entity.GenTableColumn;
import com.youlai.boot.codegen.model.query.TableQuery;
import com.youlai.boot.codegen.model.vo.CodegenPreviewVO;
import com.youlai.boot.codegen.model.vo.TablePageVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 代码生成服务实现类。
*
* <p>
* 根据代码生成配置({@link CodegenProperties})与表/字段元数据,渲染模板并提供预览与下载能力。
* </p>
*
* @author Ray
* @since 2.10.0
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class CodegenServiceImpl implements CodegenService {
private final DatabaseMapper databaseMapper;
private final CodegenProperties codegenProperties;
private final GenTableService genTableService;
private final GenTableColumnService genTableColumnService;
/**
* 数据表分页列表
*
* @param queryParams 查询参数
* @return 分页结果
*/
public Page<TablePageVO> getTablePage(TableQuery queryParams) {
Page<TablePageVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
// 设置排除的表
List<String> excludeTables = codegenProperties.getExcludeTables();
queryParams.setExcludeTables(excludeTables);
return databaseMapper.getTablePage(page, queryParams);
}
/**
* 解析前端模板路径
*
* @param templateName 模板标识
* @param templateConfig 模板配置
* @param frontendType 前端类型
* @return 模板路径
*/
private String resolveFrontendTemplatePath(String templateName,
CodegenProperties.TemplateConfig templateConfig,
String frontendType) {
if (!"js".equals(frontendType)) {
return templateConfig.getTemplatePath();
}
if ("API".equals(templateName)) {
return "codegen/frontend/js/api.js.vm";
}
if ("VIEW".equals(templateName)) {
return "codegen/frontend/js/index.js.vue.vm";
}
return templateConfig.getTemplatePath();
}
/**
* 解析前端文件后缀
*
* @param templateName 模板标识
* @param templateConfig 模板配置
* @param frontendType 前端类型
* @return 文件后缀
*/
private String resolveFrontendExtension(String templateName,
CodegenProperties.TemplateConfig templateConfig,
String frontendType) {
if (!"js".equals(frontendType)) {
return templateConfig.getExtension();
}
if ("API".equals(templateName) || "API_TYPES".equals(templateName)) {
return ".js";
}
return templateConfig.getExtension();
}
/**
* 获取预览生成代码
*
* @param tableName 表名
* @return 预览数据
*/
@Override
public List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType, String type) {
List<CodegenPreviewVO> list = new ArrayList<>();
GenTable genTable = genTableService.getOne(new LambdaQueryWrapper<GenTable>()
.eq(GenTable::getTableName, tableName)
);
if (genTable == null) {
throw new BusinessException("未找到表生成配置");
}
List<GenTableColumn> fieldConfigs = genTableColumnService.list(new LambdaQueryWrapper<GenTableColumn>()
.eq(GenTableColumn::getTableId, genTable.getId())
.orderByAsc(GenTableColumn::getFieldSort)
);
if (CollectionUtil.isEmpty(fieldConfigs)) {
throw new BusinessException("未找到字段生成配置");
}
// 遍历模板配置
Map<String, CodegenProperties.TemplateConfig> templateConfigs = codegenProperties.getTemplateConfigs();
String frontendType = StrUtil.blankToDefault(type, "ts").toLowerCase();
for (Map.Entry<String, CodegenProperties.TemplateConfig> templateConfigEntry : templateConfigs.entrySet()) {
CodegenPreviewVO previewVo = new CodegenPreviewVO();
CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue();
String templateName = templateConfigEntry.getKey();
if ("js".equals(frontendType) && "API_TYPES".equals(templateName)) {
continue;
}
String effectiveTemplatePath = resolveFrontendTemplatePath(templateName, templateConfig, frontendType);
String extension = resolveFrontendExtension(templateName, templateConfig, frontendType);
/* 1. 生成文件名 UserController */
// User Role Menu Dept
String entityName = genTable.getEntityName();
// Controller Service Mapper Entity
// .java .ts .vue
// 文件名 UserController.java
String fileName = getFileName(entityName, templateName, extension);
previewVo.setFileName(fileName);
previewVo.setScope(resolveScope(templateName));
previewVo.setLanguage(resolveLanguage(fileName));
/* 2. 生成文件路径 */
// 包名com.youlai.boot
String packageName = genTable.getPackageName();
// 模块名system
String moduleName = genTable.getModuleName();
// 子包名controller
String subpackageName = templateConfig.getSubpackageName();
// 组合成文件路径src/main/java/com/youlai/boot/system/controller
String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName);
previewVo.setPath(filePath);
/* 3. 生成文件内容 */
// 将模板文件中的变量替换为具体的值 生成代码内容
// 优先使用保存的 ui没有则使用请求参数
String finalType = StrUtil.blankToDefault(genTable.getPageType(), pageType);
String content = getCodeContent(
effectiveTemplatePath,
templateConfig.getSubpackageName(),
genTable,
fieldConfigs,
finalType
);
previewVo.setContent(content);
list.add(previewVo);
}
return list;
}
private String resolveScope(String templateName) {
return switch (templateName) {
case "API", "API_TYPES", "VIEW" -> "frontend";
default -> "backend";
};
}
private String resolveLanguage(String fileName) {
return FileNameUtil.extName(fileName).toLowerCase();
}
/**
* 生成文件名。
*
* <p>部分模板需要使用约定的命名规则(例如前端 API 文件)。</p>
*
* @param entityName 实体名(例如 User
* @param templateName 模板名(例如 Entity、Controller、API
* @param extension 文件后缀(例如 .java、.ts
* @return 文件名
*/
private String getFileName(String entityName, String templateName, String extension) {
if ("Entity".equals(templateName)) {
return entityName + extension;
} else if ("MapperXml".equals(templateName)) {
return entityName + "Mapper" + extension;
} else if ("API".equals(templateName)) {
return "index" + extension;
} else if ("API_TYPES".equals(templateName)) {
return "types" + extension;
} else if ("VIEW".equals(templateName)) {
return "index.vue";
}
return entityName + templateName + extension;
}
/**
* 生成文件路径。
*
* @param templateName 模板名
* @param moduleName 模块名(例如 system
* @param packageName 包名(例如 com.youlai.boot
* @param subPackageName 子包名(例如 controller、service.impl、api、views
* @param entityName 实体名(例如 User
* @return 生成文件路径
*/
private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) {
String path;
if ("MapperXml".equals(templateName)) {
path = (codegenProperties.getBackendAppName()
+ File.separator
+ "src" + File.separator + "main" + File.separator + "resources"
+ File.separator + subPackageName
+ File.separator + moduleName
);
} else if ("API".equals(templateName)) {
path = (codegenProperties.getFrontendAppName()
+ File.separator + "src"
+ File.separator + "api"
+ File.separator + moduleName
+ File.separator + StrUtil.toSymbolCase(entityName, '-')
);
} else if ("API_TYPES".equals(templateName)) {
path = (codegenProperties.getFrontendAppName()
+ File.separator + "src"
+ File.separator + "api"
+ File.separator + moduleName
+ File.separator + StrUtil.toSymbolCase(entityName, '-')
);
} else if ("VIEW".equals(templateName)) {
// path = "src/views/system/user";
path = (codegenProperties.getFrontendAppName()
+ File.separator + "src"
+ File.separator + subPackageName
+ File.separator + moduleName
+ File.separator + StrUtil.toSymbolCase(entityName, '-')
);
} else {
path = (codegenProperties.getBackendAppName()
+ File.separator
+ "src" + File.separator + "main" + File.separator + "java"
+ File.separator + packageName
+ File.separator + moduleName
+ File.separator + subPackageName
);
}
// subPackageName = model.entity => model/entity
path = path.replace(".", File.separator);
return path;
}
/**
* 渲染模板,生成代码内容。
*
* @param templateConfig 模板配置
* @param genTable 表生成配置
* @param fieldConfigs 字段配置
* @param pageType 前端页面类型
* @return 渲染后的代码内容
*/
private String getCodeContent(String templatePath,
String subpackageName,
GenTable genTable,
List<GenTableColumn> fieldConfigs,
String pageType) {
Map<String, Object> bindMap = new HashMap<>();
String entityName = genTable.getEntityName();
bindMap.put("packageName", genTable.getPackageName());
bindMap.put("moduleName", genTable.getModuleName());
bindMap.put("subpackageName", subpackageName);
bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm"));
bindMap.put("entityName", entityName);
bindMap.put("tableName", genTable.getTableName());
bindMap.put("author", genTable.getAuthor());
String entityLowerCamel = StrUtil.lowerFirst(entityName);
String entityKebab = StrUtil.toSymbolCase(entityName, '-');
String entityUpperSnake = StrUtil.toSymbolCase(entityName, '_').toUpperCase();
bindMap.put("entityLowerCamel", entityLowerCamel);
bindMap.put("entityKebab", entityKebab);
bindMap.put("entityUpperSnake", entityUpperSnake);
bindMap.put("businessName", genTable.getBusinessName());
bindMap.put("entityComment", genTable.getBusinessName());
bindMap.put("fieldConfigs", fieldConfigs);
boolean hasLocalDateTime = false;
boolean hasBigDecimal = false;
boolean hasRequiredField = false;
for (GenTableColumn fieldConfig : fieldConfigs) {
if (StrUtil.isBlank(fieldConfig.getFieldType())) {
fieldConfig.setFieldType(JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()));
}
if ("LocalDateTime".equals(fieldConfig.getFieldType()) || "LocalDate".equals(fieldConfig.getFieldType())) {
hasLocalDateTime = true;
}
if ("BigDecimal".equals(fieldConfig.getFieldType())) {
hasBigDecimal = true;
}
if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) {
hasRequiredField = true;
}
fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType()));
}
bindMap.put("hasLocalDateTime", hasLocalDateTime);
bindMap.put("hasBigDecimal", hasBigDecimal);
bindMap.put("hasRequiredField", hasRequiredField);
TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH));
// 根据 ui 选择不同的前端页面模板:默认 index.vue.vm封装版使用 index.curd.vue.vm
String path = templatePath;
if ("curd".equalsIgnoreCase(pageType)) {
if (path.endsWith("index.js.vue.vm")) {
path = path.replace("index.js.vue.vm", "index.curd.js.vue.vm");
} else if (path.endsWith("index.vue.vm")) {
path = path.replace("index.vue.vm", "index.curd.vue.vm");
}
}
Template template = templateEngine.getTemplate(path);
return template.render(bindMap);
}
/**
* 下载代码。
*
* @param tableNames 表名数组,支持多张表
* @param ui 页面类型
* @return zip 压缩文件字节数组
*/
@Override
public byte[] downloadCode(String[] tableNames, String ui, String type) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ZipOutputStream zip = new ZipOutputStream(outputStream)) {
// 遍历每个表名,生成对应的代码并压缩到 zip 文件中
for (String tableName : tableNames) {
generateAndZipCode(tableName, zip, ui, type);
}
// 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整
zip.finish();
return outputStream.toByteArray();
} catch (IOException e) {
log.error("Error while generating zip for code download", e);
throw new RuntimeException("Failed to generate code zip file1", e);
}
}
/**
* 根据表名生成代码并压缩到 zip 文件中。
*
* @param tableName 表名
* @param zip 压缩文件输出流
* @param ui 页面类型
*/
private void generateAndZipCode(String tableName, ZipOutputStream zip, String ui, String type) {
List<CodegenPreviewVO> codePreviewList = getCodegenPreviewData(tableName, ui, type);
for (CodegenPreviewVO codePreview : codePreviewList) {
String fileName = codePreview.getFileName();
String content = codePreview.getContent();
String path = codePreview.getPath();
try {
// 创建压缩条目
ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName);
zip.putNextEntry(zipEntry);
// 写入文件内容
zip.write(content.getBytes(StandardCharsets.UTF_8));
// 关闭当前压缩条目
zip.closeEntry();
} catch (IOException e) {
log.error("Error while adding file1 {} to zip", fileName, e);
}
}
}
}

View File

@@ -0,0 +1,21 @@
package com.youlai.boot.codegen.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.codegen.mapper.GenTableColumnMapper;
import com.youlai.boot.codegen.model.entity.GenTableColumn;
import com.youlai.boot.codegen.service.GenTableColumnService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 代码生成字段配置服务实现类
*
* @author Ray.Hao
* @since 2.10.0
*/
@Service
@RequiredArgsConstructor
public class GenTableColumnServiceImpl extends ServiceImpl<GenTableColumnMapper, GenTableColumn> implements GenTableColumnService {
}

View File

@@ -0,0 +1,228 @@
package com.youlai.boot.codegen.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.common.enums.EnvEnum;
import com.youlai.boot.codegen.enums.FormTypeEnum;
import com.youlai.boot.codegen.enums.JavaTypeEnum;
import com.youlai.boot.codegen.enums.QueryTypeEnum;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.codegen.config.CodegenProperties;
import com.youlai.boot.codegen.converter.CodegenConverter;
import com.youlai.boot.codegen.mapper.DatabaseMapper;
import com.youlai.boot.codegen.mapper.GenTableMapper;
import com.youlai.boot.codegen.model.vo.ColumnMetaVO;
import com.youlai.boot.codegen.model.vo.TableMetaVO;
import com.youlai.boot.codegen.model.entity.GenTable;
import com.youlai.boot.codegen.model.entity.GenTableColumn;
import com.youlai.boot.codegen.model.form.GenConfigForm;
import com.youlai.boot.codegen.service.GenTableService;
import com.youlai.boot.codegen.service.GenTableColumnService;
import com.youlai.boot.common.event.CodegenMenuEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
/**
* 数据库服务实现类
*
* @author Ray
* @since 2.10.0
*/
@Service
@RequiredArgsConstructor
public class GenTableServiceImpl extends ServiceImpl<GenTableMapper, GenTable> implements GenTableService {
private final DatabaseMapper databaseMapper;
private final CodegenProperties codegenProperties;
private final GenTableColumnService genTableColumnService;
private final CodegenConverter codegenConverter;
@Value("${spring.profiles.active}")
private String springProfilesActive;
private final ApplicationEventPublisher eventPublisher;
/**
* 获取代码生成配置
*
* @param tableName 表名 eg: sys_user
* @return 代码生成配置
*/
@Override
public GenConfigForm getGenTableFormData(String tableName) {
// 查询表生成配置
GenTable genTable = this.getOne(
new LambdaQueryWrapper<>(GenTable.class)
.eq(GenTable::getTableName, tableName)
.last("LIMIT 1")
);
// 是否有代码生成配置
boolean hasGenTable = genTable != null;
// 如果没有代码生成配置,则根据表的元数据生成默认配置
if (genTable == null) {
TableMetaVO tableMetadata = databaseMapper.getTableMetadata(tableName);
Assert.isTrue(tableMetadata != null, "未找到表元数据");
genTable = new GenTable();
genTable.setTableName(tableName);
// 表注释作为业务名称,去掉表字 例如:用户表 -> 用户
String tableComment = tableMetadata.getTableComment();
if (StrUtil.isNotBlank(tableComment)) {
genTable.setBusinessName(tableComment.replace("", "").trim());
}
// 根据表名生成实体类名,支持去除前缀 例如sys_user -> SysUser
String removePrefix = genTable.getRemoveTablePrefix();
String processedTable = tableName;
if (StrUtil.isNotBlank(removePrefix) && StrUtil.startWith(tableName, removePrefix)) {
processedTable = StrUtil.removePrefix(tableName, removePrefix);
}
genTable.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(processedTable))));
genTable.setPackageName(codegenProperties.getDefaultConfig().getPackageName());
genTable.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名
genTable.setAuthor(codegenProperties.getDefaultConfig().getAuthor());
}
// 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置
List<GenTableColumn> genTableColumns = new ArrayList<>();
// 获取表的列
List<ColumnMetaVO> tableColumns = databaseMapper.getTableColumns(tableName);
if (CollectionUtil.isNotEmpty(tableColumns)) {
// 查询字段生成配置
List<GenTableColumn> fieldConfigList = genTableColumnService.list(
new LambdaQueryWrapper<GenTableColumn>()
.eq(GenTableColumn::getTableId, genTable.getId())
.orderByAsc(GenTableColumn::getFieldSort)
);
Integer maxSort = fieldConfigList.stream()
.map(GenTableColumn::getFieldSort)
.filter(Objects::nonNull) // 过滤掉空值
.max(Integer::compareTo)
.orElse(0);
for (ColumnMetaVO tableColumn : tableColumns) {
// 根据列名获取字段生成配置
String columnName = tableColumn.getColumnName();
GenTableColumn fieldConfig = fieldConfigList.stream()
.filter(item -> StrUtil.equals(item.getColumnName(), columnName))
.findFirst()
.orElseGet(() -> createDefaultFieldConfig(tableColumn));
if (fieldConfig.getFieldSort() == null) {
fieldConfig.setFieldSort(++maxSort);
}
// 根据列类型设置字段类型
String fieldType = fieldConfig.getFieldType();
if (StrUtil.isBlank(fieldType)) {
String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType());
fieldConfig.setFieldType(javaType);
}
// 如果没有代码生成配置,则默认展示在列表和表单
if (!hasGenTable) {
fieldConfig.setIsShowInList(1);
fieldConfig.setIsShowInForm(1);
}
genTableColumns.add(fieldConfig);
}
}
// 对 genTableColumns 按照 fieldSort 排序
genTableColumns = genTableColumns.stream().sorted(Comparator.comparing(GenTableColumn::getFieldSort)).toList();
GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genTable, genTableColumns);
genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName());
genConfigForm.setBackendAppName(codegenProperties.getBackendAppName());
return genConfigForm;
}
/**
* 创建默认字段配置
*
* @param columnMetaVO 表字段元数据
* @return
*/
private GenTableColumn createDefaultFieldConfig(ColumnMetaVO columnMetaVO) {
GenTableColumn fieldConfig = new GenTableColumn();
fieldConfig.setColumnName(columnMetaVO.getColumnName());
fieldConfig.setColumnType(columnMetaVO.getDataType());
fieldConfig.setFieldComment(columnMetaVO.getColumnComment());
fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaVO.getColumnName()));
fieldConfig.setIsRequired("YES".equals(columnMetaVO.getIsNullable()) ? 0 : 1);
String columnType = StrUtil.blankToDefault(fieldConfig.getColumnType(), "").toLowerCase();
if ("date".equals(columnType)) {
fieldConfig.setFormType(FormTypeEnum.DATE);
} else if ("datetime".equals(columnType) || "timestamp".equals(columnType)) {
fieldConfig.setFormType(FormTypeEnum.DATE_TIME);
} else {
fieldConfig.setFormType(FormTypeEnum.INPUT);
}
fieldConfig.setQueryType(QueryTypeEnum.EQ);
fieldConfig.setMaxLength(columnMetaVO.getCharacterMaximumLength());
return fieldConfig;
}
/**
* 保存代码生成配置
*
* @param formData 代码生成配置表单
*/
@Override
public void saveGenConfig(GenConfigForm formData) {
GenTable genTable = codegenConverter.toGenTable(formData);
this.saveOrUpdate(genTable);
// 如果选择上级菜单且当前环境不是生产环境,发布菜单创建事件
Long parentMenuId = formData.getParentMenuId();
if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) {
eventPublisher.publishEvent(new CodegenMenuEvent(this, parentMenuId,
genTable.getBusinessName(), genTable.getModuleName(), genTable.getPackageName()));
}
List<GenTableColumn> genTableColumns = codegenConverter.toGenTableColumn(formData.getFieldConfigs());
if (CollectionUtil.isEmpty(genTableColumns)) {
throw new BusinessException("字段配置不能为空");
}
genTableColumns.forEach(genTableColumn -> {
genTableColumn.setTableId(genTable.getId());
});
genTableColumnService.saveOrUpdateBatch(genTableColumns);
}
/**
* 删除代码生成配置
*
* @param tableName 表名
*/
@Override
public void deleteGenConfig(String tableName) {
GenTable genTable = this.getOne(new LambdaQueryWrapper<GenTable>()
.eq(GenTable::getTableName, tableName));
boolean result = this.remove(new LambdaQueryWrapper<GenTable>()
.eq(GenTable::getTableName, tableName)
);
if (result) {
genTableColumnService.remove(new LambdaQueryWrapper<GenTableColumn>()
.eq(GenTableColumn::getTableId, genTable.getId())
);
}
}
}

View File

@@ -0,0 +1,61 @@
# 代码生成器配置
codegen:
# 下载代码文件名称
downloadFileName: youlai-admin-code.zip
# 后端项目名称
backendAppName: youlai-boot
# 前端项目名称
frontendAppName: vue3-element-admin
# 默认配置
defaultConfig:
author: youlaitech
moduleName: system
# 排除数据表
excludeTables:
- gen_table
- gen_table_column
## 模板配置
templateConfigs:
API:
templatePath: codegen/frontend/ts/api.ts.vm
subpackageName: api
extension: .ts
API_TYPES:
templatePath: codegen/frontend/ts/api-types.ts.vm
subpackageName: types
extension: .ts
VIEW:
templatePath: codegen/frontend/ts/index.vue.vm
subpackageName: views
extension: .vue
Controller:
templatePath: codegen/backend/controller.java.vm
subpackageName: controller
Service:
templatePath: codegen/backend/service.java.vm
subpackageName: service
ServiceImpl:
templatePath: codegen/backend/serviceImpl.java.vm
subpackageName: service.impl
Mapper:
templatePath: codegen/backend/mapper.java.vm
subpackageName: mapper
MapperXml:
templatePath: codegen/backend/mapper.xml.vm
subpackageName: mapper
extension: .xml
Converter:
templatePath: codegen/backend/converter.java.vm
subpackageName: converter
Query:
templatePath: codegen/backend/query.java.vm
subpackageName: model.query
Form:
templatePath: codegen/backend/form.java.vm
subpackageName: model.form
Vo:
templatePath: codegen/backend/vo.java.vm
subpackageName: model.vo
Entity:
templatePath: codegen/backend/entity.java.vm
subpackageName: model.entity

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--suppress ALL -->
<mapper namespace="com.youlai.boot.codegen.mapper.DatabaseMapper">
<!-- 查询数据库表分页 -->
<select id="getTablePage" resultType="com.youlai.boot.codegen.model.vo.TablePageVO">
SELECT
t1.TABLE_NAME ,
t1.TABLE_COMMENT ,
t1.TABLE_COLLATION,
t1.ENGINE,
t1.CREATE_TIME,
CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured
FROM
information_schema.tables t1
LEFT JOIN gen_table t2 on t1.TABLE_NAME = t2.table_name
WHERE
t1.TABLE_SCHEMA = (SELECT DATABASE())
AND t1.table_type = 'BASE TABLE'
<if test="queryParams.keywords != null and queryParams.keywords.trim() neq ''">
AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%')
</if>
<!-- 排除的表 -->
<if test="queryParams.excludeTables != null and queryParams.excludeTables.size() > 0">
AND t1.TABLE_NAME NOT IN
<foreach collection="queryParams.excludeTables" item="excludeTable" open="(" close=")" separator=",">
#{excludeTable}
</foreach>
</if>
ORDER BY
CREATE_TIME DESC
</select>
<!-- 查询数据库表元数据 -->
<select id="getTableMetadata" resultType="com.youlai.boot.codegen.model.vo.TableMetaVO">
SELECT
TABLE_NAME ,
TABLE_COMMENT ,
TABLE_COLLATION,
ENGINE,
CREATE_TIME
FROM
information_schema.tables
WHERE
TABLE_SCHEMA = (SELECT DATABASE())
AND TABLE_NAME = #{tableName}
</select>
<!-- 获取数据库表字段元数据 -->
<select id="getTableColumns" resultType="com.youlai.boot.codegen.model.vo.ColumnMetaVO">
SELECT
COLUMN_NAME,
DATA_TYPE,
COLUMN_COMMENT,
CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey,
IS_NULLABLE,
CHARACTER_MAXIMUM_LENGTH,
CHARACTER_SET_NAME,
COLLATION_NAME
FROM
information_schema.columns
WHERE
TABLE_SCHEMA = (SELECT DATABASE())
AND TABLE_NAME = #{tableName}
ORDER BY ORDINAL_POSITION ASC
</select>
</mapper>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youlai.boot.platform.codegen.mapper.GenConfigMapper">
</mapper>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youlai.boot.platform.codegen.mapper.GenTableColumnMapper">
</mapper>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youlai.boot.platform.codegen.mapper.GenTableColumnMapper">
</mapper>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youlai.boot.platform.codegen.mapper.GenTableMapper">
</mapper>

View File

@@ -0,0 +1,81 @@
package ${packageName}.${moduleName}.${subpackageName};
import ${packageName}.${moduleName}.service.${entityName}Service;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ${packageName}.${moduleName}.model.form.${entityName}Form;
import ${packageName}.${moduleName}.model.query.${entityName}Query;
import ${packageName}.${moduleName}.model.vo.${entityName}VO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import ${packageName}.common.result.PageResult;
import ${packageName}.common.result.Result;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
/**
* $!{businessName}前端控制层
*
* @author ${author}
* @since ${date}
*/
@Tag(name = "${businessName}接口")
@RestController
@RequestMapping("/api/v1/${entityKebab}")
@RequiredArgsConstructor
public class ${entityName}Controller {
private final ${entityName}Service ${entityLowerCamel}Service;
@Operation(summary = "$!{businessName}分页列表")
@GetMapping
@PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:list')")
public PageResult<${entityName}VO> get${entityName}Page(${entityName}Query queryParams ) {
IPage<${entityName}VO> result = ${entityLowerCamel}Service.get${entityName}Page(queryParams);
return PageResult.success(result);
}
@Operation(summary = "新增${businessName}")
@PostMapping
@PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:create')")
public Result<Void> save${entityName}(@RequestBody @Valid ${entityName}Form formData ) {
boolean result = ${entityLowerCamel}Service.save${entityName}(formData);
return Result.judge(result);
}
@Operation(summary = "获取${businessName}表单数据")
@GetMapping("/{id}/form")
@PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:update')")
public Result<${entityName}Form> get${entityName}Form(
@Parameter(description = "$!{businessName}ID") @PathVariable Long id
) {
${entityName}Form formData = ${entityLowerCamel}Service.get${entityName}FormData(id);
return Result.success(formData);
}
@Operation(summary = "修改${businessName}")
@PutMapping(value = "/{id}")
@PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:update')")
public Result<Void> update${entityName}(
@Parameter(description = "$!{businessName}ID") @PathVariable Long id,
@RequestBody @Validated ${entityName}Form formData
) {
boolean result = ${entityLowerCamel}Service.update${entityName}(id, formData);
return Result.judge(result);
}
@Operation(summary = "删除${businessName}")
@DeleteMapping("/{ids}")
@PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:delete')")
public Result<Void> delete${entityName}s(
@Parameter(description = "$!{businessName}ID多个以英文逗号(,)分割") @PathVariable String ids
) {
boolean result = ${entityLowerCamel}Service.delete${entityName}s(ids);
return Result.judge(result);
}
}

View File

@@ -0,0 +1,19 @@
package ${packageName}.${moduleName}.${subpackageName};
import org.mapstruct.Mapper;
import ${packageName}.${moduleName}.model.entity.${entityName};
import ${packageName}.${moduleName}.model.form.${entityName}Form;
/**
* $!{businessName}对象转换器
*
* @author ${author}
* @since ${date}
*/
@Mapper(componentModel = "spring")
public interface ${entityName}Converter{
${entityName}Form toForm(${entityName} entity);
${entityName} toEntity(${entityName}Form formData);
}

View File

@@ -0,0 +1,37 @@
package ${packageName}.${moduleName}.${subpackageName};
import lombok.Getter;
import lombok.Setter;
#if(${hasLocalDateTime})
import java.time.LocalDateTime;
#end
#if(${hasBigDecimal})
import java.math.BigDecimal;
#end
import com.baomidou.mybatisplus.annotation.TableName;
import ${packageName}.common.base.BaseEntity;
/**
* $!{businessName}实体对象
*
* @author ${author}
* @since ${date}
*/
@Getter
@Setter
@TableName("${tableName}")
public class ${entityName} extends BaseEntity {
private static final long serialVersionUID = 1L;
#foreach($fieldConfig in ${fieldConfigs})
#if(!$fieldConfig.fieldName.equals("id") && !$fieldConfig.fieldName.equals("createTime") && !$fieldConfig.fieldName.equals("updateTime"))
#if("$!fieldConfig.fieldComment" != "")
/**
* ${fieldConfig.fieldComment}
*/
#end
private ${fieldConfig.fieldType} ${fieldConfig.fieldName};
#end
#end
}

View File

@@ -0,0 +1,62 @@
package ${packageName}.${moduleName}.${subpackageName};
import java.io.Serial;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
#if(${hasLocalDateTime})
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
#end
#if(${hasBigDecimal})
import java.math.BigDecimal;
#end
#if(${hasRequiredField})
import jakarta.validation.constraints.*;
#end
/**
* $!{businessName}表单对象
*
* @author ${author}
* @since ${date}
*/
@Getter
@Setter
@Schema(description = "$!{businessName}表单对象")
public class ${entityName}Form implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "${businessName}ID")
private Long id;
## ---------- BEGIN 字段循环遍历 ----------
#if($fieldConfigs)
#foreach($fieldConfig in ${fieldConfigs})
#if($fieldConfig.isShowInForm)
#if("$!fieldConfig.fieldComment" != "")
@Schema(description = "${fieldConfig.fieldComment}")
#end
#if($fieldConfig.isRequired)
#if($fieldConfig.fieldType == 'String')
@NotBlank(message = "$fieldConfig.fieldComment不能为空")
#else
@NotNull(message = "$fieldConfig.fieldComment不能为空")
#end
#end
#if($fieldConfig.maxLength)
@Size(max=$fieldConfig.maxLength, message="$fieldConfig.fieldComment长度不能超过${fieldConfig.maxLength}个字符")
#end
#if($fieldConfig.fieldType == 'LocalDateTime')
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
#end
private ${fieldConfig.fieldType} ${fieldConfig.fieldName};
#end
#end
#end
}

View File

@@ -0,0 +1,28 @@
package ${packageName}.${moduleName}.${subpackageName};
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import ${packageName}.${moduleName}.model.entity.${entityName};
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import ${packageName}.${moduleName}.model.query.${entityName}Query;
import ${packageName}.${moduleName}.model.vo.${entityName}VO;
import org.apache.ibatis.annotations.Mapper;
/**
* $!{businessName}Mapper接口
*
* @author ${author}
* @since ${date}
*/
@Mapper
public interface ${entityName}Mapper extends BaseMapper<${entityName}> {
/**
* 获取${businessName}分页数据
*
* @param page 分页对象
* @param queryParams 查询参数
* @return {@link Page<${entityName}VO>} $!{businessName}分页列表
*/
Page<${entityName}VO> get${entityName}Page(Page<${entityName}VO> page, ${entityName}Query queryParams);
}

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${packageName}.${moduleName}.mapper.${entityName}Mapper">
<!-- 获取${businessName}分页列表 -->
<select id="get${entityName}Page" resultType="${packageName}.${moduleName}.model.vo.${entityName}VO">
SELECT
#if($fieldConfigs)
#set ($fields = [])
#foreach($fieldConfig in ${fieldConfigs})
#if($fieldConfig.isShowInList)
#set ($discard = $fields.add(${fieldConfig.columnName}))
#end
#end
#foreach($field in $fields)
${field}#if($foreach.hasNext),#end
#end
#end
FROM
${tableName}
<where>
#if($fieldConfigs)
#foreach($fieldConfig in ${fieldConfigs})
#if($fieldConfig.isShowInQuery)
#if($fieldConfig.fieldType == "String")
<if test="queryParams.${fieldConfig.fieldName} != null and queryParams.${fieldConfig.fieldName} != ''">
#else
<if test="queryParams.${fieldConfig.fieldName} != null">
#end
#set ($queryType = ${fieldConfig.queryType}.name())
#if($queryType == "EQ")
AND ${fieldConfig.columnName} = #{queryParams.${fieldConfig.fieldName}}
#elseif($queryType == "LIKE")
AND ${fieldConfig.columnName} LIKE CONCAT('%', #{queryParams.${fieldConfig.fieldName}}, '%')
#elseif($queryType == "IN")
<if test="queryParams.${fieldConfig.fieldName}.size() > 0">
AND ${fieldConfig.columnName} IN <foreach collection="queryParams.${fieldConfig.fieldName}" item="item" open="(" close=")" separator=",">#{item}</foreach>
</if>
#elseif($queryType == "BETWEEN")
#if($fieldConfig.formType.name() == "DATE" || $fieldConfig.formType.name() == "DATE_TIME")
<if test="queryParams.${fieldConfig.fieldName}[0] != null and queryParams.${fieldConfig.fieldName}[0] != ''">
<bind name="startDate" value="queryParams.${fieldConfig.fieldName}[0].length() == 10 ? queryParams.${fieldConfig.fieldName}[0] + ' 00:00:00' : queryParams.${fieldConfig.fieldName}[0]"/>
AND ${fieldConfig.columnName} &gt;= #{startDate}
</if>
<if test="queryParams.${fieldConfig.fieldName}[1] != null and queryParams.${fieldConfig.fieldName}[1] != ''">
<bind name="endDate" value="queryParams.${fieldConfig.fieldName}[1].length() == 10 ? queryParams.${fieldConfig.fieldName}[1] + ' 23:59:59' : queryParams.${fieldConfig.fieldName}[1]"/>
AND ${fieldConfig.columnName} &lt;= #{endDate}
</if>
#else
<if test="queryParams.${fieldConfig.fieldName}[0] != null and queryParams.${fieldConfig.fieldName}[0] != ''">
AND ${fieldConfig.columnName} &gt;= #{queryParams.${fieldConfig.fieldName}[0]}
</if>
<if test="queryParams.${fieldConfig.fieldName}[1] != null and queryParams.${fieldConfig.fieldName}[1] != ''">
AND ${fieldConfig.columnName} &lt;= #{queryParams.${fieldConfig.fieldName}[1]}
</if>
#end
#elseif($queryType == "GT")
AND ${fieldConfig.columnName} > #{queryParams.${fieldConfig.fieldName}}
#elseif($queryType == "GE")
AND ${fieldConfig.columnName} >= #{queryParams.${fieldConfig.fieldName}}
#elseif($queryType == "LT")
AND ${fieldConfig.columnName} &lt; #{queryParams.${fieldConfig.fieldName}}
#elseif($queryType == "LE")
AND ${fieldConfig.columnName} &lt;= #{queryParams.${fieldConfig.fieldName}}
#elseif($queryType == "NE")
AND ${fieldConfig.columnName} != #{queryParams.${fieldConfig.fieldName}}
#elseif($queryType == "LIKE_LEFT")
AND ${fieldConfig.columnName} LIKE CONCAT('%', #{queryParams.${fieldConfig.fieldName}})
#elseif($queryType == "LIKE_RIGHT")
AND ${fieldConfig.columnName} LIKE CONCAT(#{queryParams.${fieldConfig.fieldName}}, '%')
#end
</if>
#end
#end
#end
</where>
</select>
</mapper>

View File

@@ -0,0 +1,44 @@
package ${packageName}.${moduleName}.${subpackageName};
import ${packageName}.common.base.BaseQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
#if(${hasLocalDateTime})
import java.time.LocalDateTime;
#end
#if(${hasBigDecimal})
import java.math.BigDecimal;
#end
/**
* $!{businessName}查询对象
*
* @author ${author}
* @since ${date}
*/
@Schema(description ="$!{businessName}查询对象")
@Getter
@Setter
public class ${entityName}Query extends BaseQuery {
#if($fieldConfigs)
#foreach($fieldConfig in ${fieldConfigs})
#if($fieldConfig.isShowInQuery)
#if("$!fieldConfig.fieldComment" != "")
@Schema(description = "${fieldConfig.fieldComment}")
#end
#if($fieldConfig.formType.name() == "DATE" || $fieldConfig.formType.name() == "DATE_TIME")
#if($fieldConfig.queryType.name() == "BETWEEN")
private List<String> ${fieldConfig.fieldName};
#else
private ${fieldConfig.fieldType} ${fieldConfig.fieldName};
#end
#else
private ${fieldConfig.fieldType} ${fieldConfig.fieldName};
#end
#end
#end
#end
}

View File

@@ -0,0 +1,58 @@
package ${packageName}.${moduleName}.${subpackageName};
import ${packageName}.${moduleName}.model.entity.${entityName};
import ${packageName}.${moduleName}.model.form.${entityName}Form;
import ${packageName}.${moduleName}.model.query.${entityName}Query;
import ${packageName}.${moduleName}.model.vo.${entityName}VO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* $!{businessName}服务类
*
* @author ${author}
* @since ${date}
*/
public interface ${entityName}Service extends IService<${entityName}> {
/**
*$!{businessName}分页列表
*
* @return {@link IPage<${entityName}VO>} $!{businessName}分页列表
*/
IPage<${entityName}VO> get${entityName}Page(${entityName}Query queryParams);
/**
* 获取${businessName}表单数据
*
* @param id $!{businessName}ID
* @return ${businessName}表单数据
*/
${entityName}Form get${entityName}FormData(Long id);
/**
* 新增${businessName}
*
* @param formData $!{businessName}表单对象
* @return 是否新增成功
*/
boolean save${entityName}(${entityName}Form formData);
/**
* 修改${businessName}
*
* @param id $!{businessName}ID
* @param formData $!{businessName}表单对象
* @return 是否修改成功
*/
boolean update${entityName}(Long id, ${entityName}Form formData);
/**
* 删除${businessName}
*
* @param ids $!{businessName}ID多个以英文逗号(,)分割
* @return 是否删除成功
*/
boolean delete${entityName}s(String ids);
}

View File

@@ -0,0 +1,103 @@
package ${packageName}.${moduleName}.${subpackageName};
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import ${packageName}.${moduleName}.mapper.${entityName}Mapper;
import ${packageName}.${moduleName}.service.${entityName}Service;
import ${packageName}.${moduleName}.model.entity.${entityName};
import ${packageName}.${moduleName}.model.form.${entityName}Form;
import ${packageName}.${moduleName}.model.query.${entityName}Query;
import ${packageName}.${moduleName}.model.vo.${entityName}VO;
import ${packageName}.${moduleName}.converter.${entityName}Converter;
import java.util.Arrays;
import java.util.List;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
/**
* $!{businessName}服务实现类
*
* @author ${author}
* @since ${date}
*/
@Service
@RequiredArgsConstructor
public class ${entityName}ServiceImpl extends ServiceImpl<${entityName}Mapper, ${entityName}> implements ${entityName}Service {
private final ${entityName}Converter ${entityLowerCamel}Converter;
/**
* 获取${businessName}分页列表
*
* @param queryParams 查询参数
* @return {@link IPage<${entityName}VO>} $!{businessName}分页列表
*/
@Override
public IPage<${entityName}VO> get${entityName}Page(${entityName}Query queryParams) {
Page<${entityName}VO> pageResult = this.baseMapper.get${entityName}Page(
new Page<>(queryParams.getPageNum(), queryParams.getPageSize()),
queryParams
);
return pageResult;
}
/**
* 获取${businessName}表单数据
*
* @param id $!{businessName}ID
* @return ${businessName}表单数据
*/
@Override
public ${entityName}Form get${entityName}FormData(Long id) {
${entityName} entity = this.getById(id);
return ${entityLowerCamel}Converter.toForm(entity);
}
/**
* 新增${businessName}
*
* @param formData $!{businessName}表单对象
* @return 是否新增成功
*/
@Override
public boolean save${entityName}(${entityName}Form formData) {
${entityName} entity = ${entityLowerCamel}Converter.toEntity(formData);
return this.save(entity);
}
/**
* 更新${businessName}
*
* @param id $!{businessName}ID
* @param formData $!{businessName}表单对象
* @return 是否修改成功
*/
@Override
public boolean update${entityName}(Long id,${entityName}Form formData) {
${entityName} entity = ${entityLowerCamel}Converter.toEntity(formData);
entity.setId(id);
return this.updateById(entity);
}
/**
* 删除${businessName}
*
* @param ids $!{businessName}ID多个以英文逗号(,)分割
* @return 是否删除成功
*/
@Override
public boolean delete${entityName}s(String ids) {
Assert.isTrue(StrUtil.isNotBlank(ids), "删除的${businessName}数据为空");
// 逻辑删除
List<Long> idList = Arrays.stream(ids.split(","))
.map(Long::parseLong)
.toList();
return this.removeByIds(idList);
}
}

View File

@@ -0,0 +1,39 @@
package ${packageName}.${moduleName}.${subpackageName};
import java.io.Serial;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
#if(${hasLocalDateTime})
import java.time.LocalDateTime;
#end
#if(${hasBigDecimal})
import java.math.BigDecimal;
#end
/**
* $!{businessName}视图对象
*
* @author ${author}
* @since ${date}
*/
@Getter
@Setter
@Schema( description = "$!{businessName}视图对象")
public class ${entityName}VO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
#if($fieldConfigs)
#foreach($fieldConfig in ${fieldConfigs})
#if($fieldConfig.isShowInList)
#if("$!fieldConfig.fieldComment" != "")
@Schema(description = "${fieldConfig.fieldComment}")
#end
private ${fieldConfig.fieldType} ${fieldConfig.fieldName};
#end
#end
#end
}

View File

@@ -0,0 +1,63 @@
import request from "@/utils/request";
const ${entityUpperSnake}_BASE_URL = "/api/v1/${entityKebab}";
const ${entityName}API = {
/** 获取${businessName}分页数据 */
getPage(queryParams) {
return request({
url: `${${entityUpperSnake}_BASE_URL}`,
method: "get",
params: queryParams,
});
},
/**
* 获取${businessName}表单数据
* @param {string} id ${businessName}ID
*/
getFormData(id) {
return request({
url: `${${entityUpperSnake}_BASE_URL}/${id}/form`,
method: "get",
});
},
/**
* 新增${businessName}
* @param {Object} data ${businessName}表单数据
*/
create(data) {
return request({
url: `${${entityUpperSnake}_BASE_URL}`,
method: "post",
data,
});
},
/**
* 更新${businessName}
* @param {string} id ${businessName}ID
* @param {Object} data ${businessName}表单数据
*/
update(id, data) {
return request({
url: `${${entityUpperSnake}_BASE_URL}/${id}`,
method: "put",
data,
});
},
/**
* 批量删除${businessName},多个以英文逗号分割
* @param {string} ids ${businessName}ID字符串
*/
deleteByIds(ids) {
return request({
url: `${${entityUpperSnake}_BASE_URL}/${ids}`,
method: "delete",
});
},
};
export default ${entityName}API;

View File

@@ -0,0 +1,369 @@
<template>
<div class="page-container h-full flex flex-1 flex-col">
<!-- 搜索 -->
<PageSearch
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
>
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInQuery == 1 && $fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<template #$fieldConfig.fieldName="scope">
<Dict v-model="scope.formData[scope.prop]" code="$fieldConfig.dictType" v-bind="scope.attrs" />
</template>
#end
#end
</PageSearch>
<!-- 列表 -->
<PageContent
ref="contentRef"
:content-config="contentConfig"
@add-click="handleAddClick"
@export-click="handleExportClick"
@search-click="handleSearchClick"
@toolbar-click="handleToolbarClick"
@operate-click="handleOperateClick"
@filter-change="handleFilterChange"
>
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInList == 1 && $fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<template #$fieldConfig.fieldName="scope">
<DictLabel v-model="scope.row[scope.prop]" code="$fieldConfig.dictType" />
</template>
#end
#end
</PageContent>
<!-- 新增 -->
<PageModal ref="addModalRef" :modal-config="addModalConfig" @submit-click="handleSubmitClick">
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInForm == 1 && $fieldConfig.formType.name() != "HIDDEN" && $fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<template #$fieldConfig.fieldName="scope">
<Dict v-model="scope.formData[scope.prop]" code="$fieldConfig.dictType" v-bind="scope.attrs" />
</template>
#end
#end
</PageModal>
<!-- 编辑 -->
<PageModal ref="editModalRef" :modal-config="editModalConfig" @submit-click="handleSubmitClick">
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInForm == 1 && $fieldConfig.formType.name() != "HIDDEN" && $fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<template #$fieldConfig.fieldName="scope">
<Dict v-model="scope.formData[scope.prop]" code="$fieldConfig.dictType" v-bind="scope.attrs" />
</template>
#end
#end
</PageModal>
</div>
</template>
<script setup>
defineOptions({ name: "$entityName" });
import ${entityName}API from "@/api/${moduleName}/${entityKebab}";
import usePage from "@/components/CURD/usePage";
// 组合式 CRUD
const {
searchRef,
contentRef,
addModalRef,
editModalRef,
handleQueryClick,
handleResetClick,
handleAddClick,
handleEditClick,
handleSubmitClick,
handleExportClick,
handleSearchClick,
handleFilterChange,
} = usePage();
// 搜索配置
const searchConfig = reactive({
permPrefix: "${moduleName}:${entityKebab}",
formItems: [
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInQuery == 1)
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
{
type: "custom",
slotName: "$fieldConfig.fieldName",
label: "$fieldConfig.fieldComment",
prop: "$fieldConfig.fieldName",
attrs: {
placeholder: "$fieldConfig.fieldComment",
clearable: true,
style: { width: "200px" },
},
},
#else
{
#if($fieldConfig.javaType == "String")
type: "input",
#elseif($fieldConfig.javaType == "Integer" || $fieldConfig.javaType == "Long")
type: "input-number",
#elseif($fieldConfig.javaType == "Date" || $fieldConfig.javaType == "LocalDateTime")
type: "date-picker",
#else
type: "input",
#end
label: "$fieldConfig.fieldComment",
prop: "$fieldConfig.fieldName",
attrs: {
placeholder: "$fieldConfig.fieldComment",
#if($fieldConfig.javaType == "Date" || $fieldConfig.javaType == "LocalDateTime")
type: "daterange",
"range-separator": "~",
"start-placeholder": "开始时间",
"end-placeholder": "截止时间",
"value-format": "YYYY-MM-DD",
#end
clearable: true,
style: { width: "200px" },
},
},
#end
#end
#end
],
});
// 列表配置
const contentConfig = reactive({
// 权限前缀
permPrefix: "${moduleName}:${entityKebab}",
table: {
border: true,
highlightCurrentRow: true,
},
// 主键
pk: "id",
// 列表查询接口
indexAction: ${entityName}API.getPage,
// 删除接口
deleteAction: ${entityName}API.deleteByIds,
// 数据解析函数
parseData(res) {
return {
total: res?.total ?? 0,
list: res?.list ?? [],
};
},
// 分页配置
pagination: {
background: true,
layout: "total, sizes, prev, pager, next, jumper",
pageSize: 20,
pageSizes: [10, 20, 30, 50],
},
// 工具栏配置
toolbar: ["add", "delete"],
defaultToolbar: ["refresh", "filter"],
// 表格列配置
cols: [
{ type: "selection", width: 55, align: "center" },
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInList == 1)
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
{
label: "$fieldConfig.fieldComment",
prop: "$fieldConfig.fieldName",
templet: "custom",
slotName: "$fieldConfig.fieldName"
},
#else
{ label: "$fieldConfig.fieldComment", prop: "$fieldConfig.fieldName" },
#end
#end
#end
{
label: "操作",
prop: "operation",
width: 220,
templet: "tool",
operat: ["edit", "delete"],
},
],
});
// 新增配置
const addModalConfig = reactive({
// 权限前缀
permPrefix: "${moduleName}:${entityKebab}",
// 主键
pk: "id",
// 弹窗配置
dialog: {
title: "新增$!{entityComment}",
width: 800,
draggable: true,
},
form: {
labelWidth: 100,
},
// 表单项配置
formItems: [
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInForm == 1 && $fieldConfig.formType.name() != "HIDDEN")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
{
type: "custom",
label: "$fieldConfig.fieldComment",
prop: "$fieldConfig.fieldName",
slotName: "$fieldConfig.fieldName",
attrs: {
placeholder: "$fieldConfig.fieldComment",
style: { width: "100%" }
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "change" }],
#end
},
#else
{
#if($fieldConfig.javaType == "String")
#if($fieldConfig.fieldName.contains("password") || $fieldConfig.fieldName.contains("pwd"))
type: "input",
attrs: {
type: "password",
placeholder: "$fieldConfig.fieldComment",
showPassword: true
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" }],
#end
#elseif($fieldConfig.fieldName.contains("email"))
type: "input",
attrs: {
placeholder: "$fieldConfig.fieldComment",
maxlength: 50
},
rules: [
#if($fieldConfig.isRequired == 1)
{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" },
#end
{ pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/, message: "请输入正确的邮箱地址", trigger: "blur" }
],
#elseif($fieldConfig.fieldName.contains("mobile") || $fieldConfig.fieldName.contains("phone"))
type: "input",
attrs: {
placeholder: "$fieldConfig.fieldComment",
maxlength: 11
},
rules: [
#if($fieldConfig.isRequired == 1)
{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" },
#end
{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }
],
#else
type: "input",
attrs: {
placeholder: "$fieldConfig.fieldComment"
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" }],
#end
#end
#elseif($fieldConfig.javaType == "Integer" || $fieldConfig.javaType == "Long")
type: "input-number",
attrs: {
placeholder: "$fieldConfig.fieldComment",
style: { width: "100%" }
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" }],
#end
#elseif($fieldConfig.javaType == "BigDecimal" || $fieldConfig.javaType == "Double")
type: "input-number",
attrs: {
placeholder: "$fieldConfig.fieldComment",
precision: 2,
style: { width: "100%" }
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" }],
#end
#elseif($fieldConfig.javaType == "Date" || $fieldConfig.javaType == "LocalDateTime")
type: "date-picker",
attrs: {
type: "datetime",
placeholder: "$fieldConfig.fieldComment",
format: "YYYY-MM-DD HH:mm:ss",
"value-format": "YYYY-MM-DD HH:mm:ss",
style: { width: "100%" }
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "change" }],
#end
#elseif($fieldConfig.javaType == "Boolean" || $fieldConfig.fieldName.contains("status"))
type: "switch",
attrs: {
activeText: "启用",
inactiveText: "禁用",
activeValue: 1,
inactiveValue: 0
},
initialValue: 1,
#else
type: "input",
attrs: {
placeholder: "$fieldConfig.fieldComment"
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" }],
#end
#end
label: "$fieldConfig.fieldComment",
prop: "$fieldConfig.fieldName",
},
#end
#end
#end
],
// 提交函数
formAction(data) {
if (data.id) {
// 编辑
return ${entityName}API.update(data.id, data);
} else {
// 新增
return ${entityName}API.create(data);
}
},
});
// 编辑配置
const editModalConfig = reactive({
permPrefix: "${moduleName}:${entityKebab}",
component: "drawer",
drawer: {
title: "编辑$!{entityComment}",
size: 500,
},
pk: "id",
formAction(data) {
return ${entityName}API.update(data.id, data);
},
formItems: addModalConfig.formItems, // 复用新增的表单项
});
// 处理操作按钮点击
const handleOperateClick = (data) => {
if (data.name === "edit") {
handleEditClick(data.row, async () => {
return await ${entityName}API.getFormData(String(data.row.id));
});
}
};
// 处理工具栏按钮点击(删除等)
const handleToolbarClick = (name) => {
console.log(name);
};
</script>

View File

@@ -0,0 +1,431 @@
<template>
<div class="page-container">
<el-card class="page-search" shadow="never">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInQuery == 1)
<el-form-item label="$fieldConfig.fieldComment" prop="$fieldConfig.fieldName">
#if($fieldConfig.formType.name() == "INPUT")
<el-input
v-model="queryParams.$fieldConfig.fieldName"
placeholder="$fieldConfig.fieldComment"
clearable
@keyup.enter="handleQuery()"
/>
#elseif($fieldConfig.formType.name() == "SELECT")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<DictSelect v-model="queryParams.$fieldConfig.fieldName" code="$fieldConfig.dictType" />
#else
<el-select v-model="queryParams.$fieldConfig.fieldName" placeholder="请选择$fieldConfig.fieldComment">
<el-option :key="1" :value="1" label="选项一"/>
<el-option :key="2" :value="2" label="选项二"/>
</el-select>
#end
#elseif($fieldConfig.formType.name() == "RADIO")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<DictSelect v-model="queryParams.$fieldConfig.fieldName" type="radio" code="$fieldConfig.dictType" />
#else
<el-radio-group v-model="queryParams.$fieldConfig.fieldName">
<el-radio :key="1" :value="1">选项一</el-radio>
<el-radio :key="2" :value="2">选项二</el-radio>
</el-radio-group>
#end
#elseif($fieldConfig.formType.name() == "CHECK_BOX")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<DictSelect v-model="queryParams.$fieldConfig.fieldName" type="checkbox" code="$fieldConfig.dictType" />
#else
<el-checkbox-group v-model="queryParams.$fieldConfig.fieldName">
<el-checkbox :key="1" :label="1">选项一</el-checkbox>
<el-checkbox :key="2" :label="2">选项二</el-checkbox>
</el-checkbox-group>
#end
#elseif($fieldConfig.formType.name() == "INPUT_NUMBER")
<el-input-number
v-model="queryParams.$fieldConfig.fieldName"
placeholder="$fieldConfig.fieldComment"
/>
#elseif($fieldConfig.formType.name() == "SWITCH")
<el-switch
v-model="queryParams.$fieldConfig.fieldName"
:active-value="1"
:inactive-value="0"
/>
#elseif($fieldConfig.formType.name() == "TEXT_AREA")
<el-input type="textarea"
v-model="queryParams.$fieldConfig.fieldName"
placeholder="$fieldConfig.fieldComment"
/>
#elseif($fieldConfig.formType.name() == "DATE_TIME")
<el-date-picker
v-model="queryParams.$fieldConfig.fieldName"
#if($fieldConfig.queryType.name() == "BETWEEN")
type="daterange"
range-separator="~"
start-placeholder="开始时间"
end-placeholder="结束时间"
#else
type="datetime"
placeholder="$fieldConfig.fieldComment"
#end
value-format="YYYY-MM-DD HH:mm:ss"
/>
#elseif($fieldConfig.formType.name() == "DATE")
<el-date-picker
class="!w-[240px]"
v-model="queryParams.$fieldConfig.fieldName"
#if($fieldConfig.queryType.name() == "BETWEEN")
type="daterange"
range-separator="~"
start-placeholder="开始时间"
end-placeholder="结束时间"
#else
type="date"
placeholder="$fieldConfig.fieldComment"
#end
value-format="YYYY-MM-DD"
/>
#end
</el-form-item>
#end
#end
<el-form-item>
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="page-content" shadow="never">
<div class="page-toolbar">
<div class="page-toolbar__left">
<el-button
v-hasPerm="['${moduleName}:${entityKebab}:create']"
type="success"
icon="plus"
@click="handleCreateClick()"
>新增</el-button>
<el-button
v-hasPerm="['${moduleName}:${entityKebab}:delete']"
type="danger"
:disabled="!hasSelection"
icon="delete"
@click="handleDelete()"
>删除</el-button>
</div>
</div>
<el-table
ref="dataTableRef"
v-loading="loading"
:data="pageData"
border
stripe
highlight-current-row
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInList == 1)
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<el-table-column label="$fieldConfig.fieldComment" width="150" align="center">
<template #default="scope">
<DictTag v-model="scope.row.$fieldConfig.fieldName" code="$fieldConfig.dictType" />
</template>
</el-table-column>
#else
<el-table-column
key="$fieldConfig.fieldName"
label="$fieldConfig.fieldComment"
prop="$fieldConfig.fieldName"
min-width="150"
align="center"
/>
#end
#end
#end
<el-table-column fixed="right" label="操作" width="220">
<template #default="scope">
<el-button
v-hasPerm="['${moduleName}:${entityKebab}:update']"
type="primary"
size="small"
link
icon="edit"
@click="handleEditClick(String(scope.row.id))"
>
编辑
</el-button>
<el-button
v-hasPerm="['${moduleName}:${entityKebab}:delete']"
type="danger"
size="small"
link
icon="delete"
@click="handleDelete(String(scope.row.id))"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="page-pagination">
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchList"
/>
</div>
</el-card>
<!-- $!{businessName}表单弹窗 -->
<el-dialog
v-model="dialog.visible"
:title="dialog.title"
width="500px"
@close="closeDialog"
>
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-width="100px">
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInForm == 1 && $fieldConfig.formType.name() != "HIDDEN")
<el-form-item label="$fieldConfig.fieldComment" prop="$fieldConfig.fieldName">
#if($fieldConfig.formType.name() == "INPUT")
<el-input
v-model="formData.$fieldConfig.fieldName"
placeholder="$fieldConfig.fieldComment"
/>
#elseif($fieldConfig.formType.name() == "SELECT")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<DictSelect v-model="formData.$fieldConfig.fieldName" code="$fieldConfig.dictType" />
#else
<el-select v-model="formData.$fieldConfig.fieldName" placeholder="请选择$fieldConfig.fieldComment">
<el-option :value="0" label="选项一"/>
<el-option :value="1" label="选项二"/>
</el-select>
#end
#elseif($fieldConfig.formType.name() == "RADIO")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<DictSelect v-model="formData.$fieldConfig.fieldName" type="radio" code="$fieldConfig.dictType" />
#else
<el-radio-group v-model="formData.$fieldConfig.fieldName">
<el-radio :value="0">选项一</el-radio>
<el-radio :value="1">选项二</el-radio>
</el-radio-group>
#end
#elseif($fieldConfig.formType.name() == "CHECK_BOX")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<DictSelect v-model="formData.$fieldConfig.fieldName" type="checkbox" code="$fieldConfig.dictType" />
#else
<el-checkbox-group v-model="formData.$fieldConfig.fieldName">
<el-checkbox :value="0">选项一</el-checkbox>
<el-checkbox :value="1">选项二</el-checkbox>
</el-checkbox-group>
#end
#elseif($fieldConfig.formType.name() == "INPUT_NUMBER")
<el-input-number
v-model="formData.$fieldConfig.fieldName"
placeholder="$fieldConfig.fieldComment"
/>
#elseif($fieldConfig.formType.name() == "SWITCH")
<el-switch
v-model="formData.$fieldConfig.fieldName"
:active-value="1"
:inactive-value="0"
/>
#elseif($fieldConfig.formType.name() == "TEXT_AREA")
<el-input type="textarea"
v-model="formData.$fieldConfig.fieldName"
placeholder="$fieldConfig.fieldComment"
/>
#elseif($fieldConfig.formType.name() == "DATE_TIME")
<el-date-picker
v-model="formData.$fieldConfig.fieldName"
type="datetime"
placeholder="$fieldConfig.fieldComment"
value-format="YYYY-MM-DD HH:mm:ss"
/>
#elseif($fieldConfig.formType.name() == "DATE")
<el-date-picker
class="!w-[240px]"
v-model="formData.$fieldConfig.fieldName"
type="date"
placeholder="$fieldConfig.fieldComment"
value-format="YYYY-MM-DD"
/>
#end
</el-form-item>
#elseif($fieldConfig.formType.name() == "HIDDEN")
<el-input type="hidden" v-model="formData.$fieldConfig.fieldName" />
#end
#end
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit()">确定</el-button>
<el-button @click="closeDialog()">取消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { useTableSelection } from "@/composables";
import ${entityName}API from "@/api/${moduleName}/${entityKebab}";
defineOptions({
name: "${entityName}",
inheritAttrs: false,
});
const queryFormRef = ref();
const dataFormRef = ref();
const loading = ref(false);
const total = ref(0);
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
});
// $!{businessName}表格数据
const pageData = ref([]);
const dialog = reactive({
title: "",
visible: false,
});
// $!{businessName}表单数据
const formData = reactive({});
// $!{businessName}表单校验规则
const rules = reactive({
#if($fieldConfigs)
#foreach($fieldConfig in ${fieldConfigs})
#if($fieldConfig.isShowInForm && $fieldConfig.isRequired)
${fieldConfig.fieldName}: [{ required: true, message: "请输入${fieldConfig.fieldComment}", trigger: "blur" }],
#end
#end
#end
});
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection();
async function fetchList() {
loading.value = true;
try {
const data = await ${entityName}API.getPage(queryParams);
pageData.value = data.list;
total.value = data.total ?? 0;
} finally {
loading.value = false;
}
}
/** 查询$!{businessName} */
function handleQuery() {
queryParams.pageNum = 1;
fetchList();
}
/** 重置$!{businessName}查询 */
function handleResetQuery() {
queryFormRef.value?.resetFields();
handleQuery();
}
function openDialog() {
dialog.visible = true;
}
function closeDialog() {
dialog.visible = false;
resetForm();
}
function resetForm() {
dataFormRef.value?.resetFields();
dataFormRef.value?.clearValidate();
formData.id = undefined;
}
async function handleCreateClick() {
dialog.title = "新增$!{businessName}";
openDialog();
}
async function handleEditClick(id) {
dialog.title = "修改$!{businessName}";
${entityName}API.getFormData(id).then((data) => {
Object.assign(formData, data);
openDialog();
});
}
/** 提交$!{businessName}表单 */
function handleSubmit() {
dataFormRef.value?.validate((valid) => {
if (valid) {
loading.value = true;
const id = formData.id;
if (id) {
${entityName}API.update(id, formData)
.then(() => {
ElMessage.success("修改成功");
closeDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
} else {
${entityName}API.create(formData)
.then(() => {
ElMessage.success("新增成功");
closeDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
}
}
});
}
/** 删除$!{businessName} */
function handleDelete(id) {
const ids = [id || selectedIds.value].join(",");
if (!ids) {
ElMessage.warning("请勾选删除项");
return;
}
ElMessageBox.confirm("确认删除已选中的数据项?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(
() => {
loading.value = true;
${entityName}API.deleteByIds(ids)
.then(() => {
ElMessage.success("删除成功");
handleResetQuery();
})
.finally(() => (loading.value = false));
},
() => {
ElMessage.info("已取消删除");
}
);
}
onMounted(() => {
handleQuery();
});
</script>

View File

@@ -0,0 +1,53 @@
/**
* ${entityName} $!{businessName}类型定义
*/
import type { BaseQueryParams } from "@/api/common";
/** $!{businessName}查询参数 */
export interface ${entityName}QueryParams extends BaseQueryParams {
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInQuery)
#if("$!fieldConfig.fieldComment" != "")
/** ${fieldConfig.fieldComment} */
#end
#if($fieldConfig.formType.name() == "DATE" || $fieldConfig.formType.name() == "DATE_TIME")
#if($fieldConfig.queryType.name() == "BETWEEN")
${fieldConfig.fieldName}?: [string, string];
#else
${fieldConfig.fieldName}?: ${fieldConfig.tsType};
#end
#else
${fieldConfig.fieldName}?: ${fieldConfig.tsType};
#end
#end
#end
}
/** $!{businessName}表单对象 */
export interface ${entityName}Form {
id?: number;
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInForm)
#if("$!fieldConfig.fieldComment" != "")
/** ${fieldConfig.fieldComment} */
#end
${fieldConfig.fieldName}?: ${fieldConfig.tsType};
#end
#end
}
/** $!{businessName}详情对象 */
export type ${entityName}Detail = ${entityName}Item & ${entityName}Form;
/** $!{businessName}列表项 */
export interface ${entityName}Item {
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInList)
#if("$!fieldConfig.fieldComment" != "")
/** ${fieldConfig.fieldComment} */
#end
${fieldConfig.fieldName}?: ${fieldConfig.tsType};
#end
#end
}

View File

@@ -0,0 +1,72 @@
import request from "@/utils/request";
import type { ${entityName}Form, ${entityName}QueryParams, ${entityName}Item } from "./types";
import type { PageResult } from "@/api/common";
const ${entityUpperSnake}_BASE_URL = "/api/v1/${entityKebab}";
const ${entityName}API = {
/** 获取${businessName}分页数据 */
getPage(queryParams?: ${entityName}QueryParams) {
return request<any, PageResult<${entityName}Item>>({
url: `${${entityUpperSnake}_BASE_URL}`,
method: "get",
params: queryParams,
});
},
/**
* 获取${businessName}表单数据
*
* @param id ${businessName}ID
* @returns ${businessName}表单数据
*/
getFormData(id: string) {
return request<any, ${entityName}Form>({
url: `${${entityUpperSnake}_BASE_URL}/${id}/form`,
method: "get",
});
},
/**
* 添加${businessName}
*
* @param data ${businessName}表单数据
*/
create(data: ${entityName}Form) {
return request({
url: `${${entityUpperSnake}_BASE_URL}`,
method: "post",
data,
});
},
/**
* 更新${businessName}
*
* @param id ${businessName}ID
* @param data ${businessName}表单数据
*/
update(id: string, data: ${entityName}Form) {
return request({
url: `${${entityUpperSnake}_BASE_URL}/${id}`,
method: "put",
data,
});
},
/**
* 批量删除${businessName},多个以英文逗号(,)分割
*
* @param ids ${businessName}ID字符串多个以英文逗号(,)分割
*/
deleteByIds(ids: string) {
return request({
url: `${${entityUpperSnake}_BASE_URL}/${ids}`,
method: "delete",
});
}
}
export default ${entityName}API;
// 重导出类型
export * from "./types";

View File

@@ -0,0 +1,371 @@
<template>
<div class="page-container h-full flex flex-1 flex-col">
<!-- 搜索 -->
<PageSearch
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
>
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInQuery == 1 && $fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<template #$fieldConfig.fieldName="scope">
<DictSelect v-model="scope.formData[scope.prop]" code="$fieldConfig.dictType" v-bind="scope.attrs" />
</template>
#end
#end
</PageSearch>
<!-- 列表 -->
<PageContent
ref="contentRef"
:content-config="contentConfig"
@add-click="handleAddClick"
@export-click="handleExportClick"
@search-click="handleSearchClick"
@toolbar-click="handleToolbarClick"
@operate-click="handleOperateClick"
@filter-change="handleFilterChange"
>
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInList == 1 && $fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<template #$fieldConfig.fieldName="scope">
<DictTag v-model="scope.row[scope.prop]" code="$fieldConfig.dictType" />
</template>
#end
#end
</PageContent>
<!-- 新增 -->
<PageModal ref="addModalRef" :modal-config="addModalConfig" @submit-click="handleSubmitClick">
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInForm == 1 && $fieldConfig.formType.name() != "HIDDEN" && $fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<template #$fieldConfig.fieldName="scope">
<DictSelect v-model="scope.formData[scope.prop]" code="$fieldConfig.dictType" v-bind="scope.attrs" />
</template>
#end
#end
</PageModal>
<!-- 编辑 -->
<PageModal ref="editModalRef" :modal-config="editModalConfig" @submit-click="handleSubmitClick">
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInForm == 1 && $fieldConfig.formType.name() != "HIDDEN" && $fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<template #$fieldConfig.fieldName="scope">
<DictSelect v-model="scope.formData[scope.prop]" code="$fieldConfig.dictType" v-bind="scope.attrs" />
</template>
#end
#end
</PageModal>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: "$entityName" });
import ${entityName}API from "@/api/${moduleName}/${entityKebab}";
import type { ${entityName}Form, ${entityName}QueryParams } from "@/api/${moduleName}/${entityKebab}";
import type { IObject, IModalConfig, IContentConfig, ISearchConfig } from "@/components/CURD/types";
import usePage from "@/components/CURD/usePage";
// 组合式 CRUD
const {
searchRef,
contentRef,
addModalRef,
editModalRef,
handleQueryClick,
handleResetClick,
handleAddClick,
handleEditClick,
handleSubmitClick,
handleExportClick,
handleSearchClick,
handleFilterChange,
} = usePage();
// 搜索配置
const searchConfig: ISearchConfig = reactive({
permPrefix: "${moduleName}:${entityKebab}",
formItems: [
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInQuery == 1)
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
{
type: "custom",
slotName: "$fieldConfig.fieldName",
label: "$fieldConfig.fieldComment",
prop: "$fieldConfig.fieldName",
attrs: {
placeholder: "$fieldConfig.fieldComment",
clearable: true,
style: { width: "200px" },
},
},
#else
{
#if($fieldConfig.javaType == "String")
type: "input",
#elseif($fieldConfig.javaType == "Integer" || $fieldConfig.javaType == "Long")
type: "input-number",
#elseif($fieldConfig.javaType == "Date" || $fieldConfig.javaType == "LocalDateTime")
type: "date-picker",
#else
type: "input",
#end
label: "$fieldConfig.fieldComment",
prop: "$fieldConfig.fieldName",
attrs: {
placeholder: "$fieldConfig.fieldComment",
#if($fieldConfig.javaType == "Date" || $fieldConfig.javaType == "LocalDateTime")
type: "daterange",
"range-separator": "~",
"start-placeholder": "开始时间",
"end-placeholder": "截止时间",
"value-format": "YYYY-MM-DD",
#end
clearable: true,
style: { width: "200px" },
},
},
#end
#end
#end
],
});
// 列表配置
const contentConfig: IContentConfig<${entityName}QueryParams> = reactive({
// 权限前缀
permPrefix: "${moduleName}:${entityKebab}",
table: {
border: true,
highlightCurrentRow: true,
},
// 主键
pk: "id",
// 列表查询接口
indexAction: ${entityName}API.getPage,
// 删除接口
deleteAction: ${entityName}API.deleteByIds,
// 数据解析函数
parseData(res: any) {
return {
total: res?.total ?? 0,
list: res?.list ?? [],
};
},
// 分页配置
pagination: {
background: true,
layout: "total, sizes, prev, pager, next, jumper",
pageSize: 20,
pageSizes: [10, 20, 30, 50],
},
// 工具栏配置
toolbar: ["add", "delete"],
defaultToolbar: ["refresh", "filter"],
// 表格列配置
cols: [
{ type: "selection", width: 55, align: "center" },
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInList == 1)
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
{
label: "$fieldConfig.fieldComment",
prop: "$fieldConfig.fieldName",
templet: "custom",
slotName: "$fieldConfig.fieldName"
},
#else
{ label: "$fieldConfig.fieldComment", prop: "$fieldConfig.fieldName" },
#end
#end
#end
{
label: "操作",
prop: "operation",
width: 220,
templet: "tool",
operat: ["edit", "delete"],
},
],
});
// 新增配置
const addModalConfig: IModalConfig<${entityName}Form> = reactive({
// 权限前缀
permPrefix: "${moduleName}:${entityKebab}",
// 主键
pk: "id",
// 弹窗配置
dialog: {
title: "新增$!{entityComment}",
width: 800,
draggable: true,
},
form: {
labelWidth: 100,
},
// 表单项配置
formItems: [
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInForm == 1 && $fieldConfig.formType.name() != "HIDDEN")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
{
type: "custom",
label: "$fieldConfig.fieldComment",
prop: "$fieldConfig.fieldName",
slotName: "$fieldConfig.fieldName",
attrs: {
placeholder: "$fieldConfig.fieldComment",
style: { width: "100%" }
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "change" }],
#end
},
#else
{
#if($fieldConfig.javaType == "String")
#if($fieldConfig.fieldName.contains("password") || $fieldConfig.fieldName.contains("pwd"))
type: "input",
attrs: {
type: "password",
placeholder: "$fieldConfig.fieldComment",
showPassword: true
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" }],
#end
#elseif($fieldConfig.fieldName.contains("email"))
type: "input",
attrs: {
placeholder: "$fieldConfig.fieldComment",
maxlength: 50
},
rules: [
#if($fieldConfig.isRequired == 1)
{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" },
#end
{ pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/, message: "请输入正确的邮箱地址", trigger: "blur" }
],
#elseif($fieldConfig.fieldName.contains("mobile") || $fieldConfig.fieldName.contains("phone"))
type: "input",
attrs: {
placeholder: "$fieldConfig.fieldComment",
maxlength: 11
},
rules: [
#if($fieldConfig.isRequired == 1)
{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" },
#end
{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }
],
#else
type: "input",
attrs: {
placeholder: "$fieldConfig.fieldComment"
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" }],
#end
#end
#elseif($fieldConfig.javaType == "Integer" || $fieldConfig.javaType == "Long")
type: "input-number",
attrs: {
placeholder: "$fieldConfig.fieldComment",
style: { width: "100%" }
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" }],
#end
#elseif($fieldConfig.javaType == "BigDecimal" || $fieldConfig.javaType == "Double")
type: "input-number",
attrs: {
placeholder: "$fieldConfig.fieldComment",
precision: 2,
style: { width: "100%" }
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" }],
#end
#elseif($fieldConfig.javaType == "Date" || $fieldConfig.javaType == "LocalDateTime")
type: "date-picker",
attrs: {
type: "datetime",
placeholder: "$fieldConfig.fieldComment",
format: "YYYY-MM-DD HH:mm:ss",
"value-format": "YYYY-MM-DD HH:mm:ss",
style: { width: "100%" }
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "change" }],
#end
#elseif($fieldConfig.javaType == "Boolean" || $fieldConfig.fieldName.contains("status"))
type: "switch",
attrs: {
activeText: "启用",
inactiveText: "禁用",
activeValue: 1,
inactiveValue: 0
},
initialValue: 1,
#else
type: "input",
attrs: {
placeholder: "$fieldConfig.fieldComment"
},
#if($fieldConfig.isRequired == 1)
rules: [{ required: true, message: "$fieldConfig.fieldComment不能为空", trigger: "blur" }],
#end
#end
label: "$fieldConfig.fieldComment",
prop: "$fieldConfig.fieldName",
},
#end
#end
#end
],
// 提交函数
formAction: (data: ${entityName}Form) => {
if (data.id) {
// 编辑
return ${entityName}API.update(data.id as string, data);
} else {
// 新增
return ${entityName}API.create(data);
}
},
});
// 编辑配置
const editModalConfig: IModalConfig<${entityName}Form> = reactive({
permPrefix: "${moduleName}:${entityKebab}",
component: "drawer",
drawer: {
title: "编辑$!{entityComment}",
size: 500,
},
pk: "id",
formAction(data: any) {
return ${entityName}API.update(data.id as string, data);
},
formItems: addModalConfig.formItems, // 复用新增的表单项
});
// 处理操作按钮点击
const handleOperateClick = (data: IObject) => {
if (data.name === "edit") {
handleEditClick(data.row, async () => {
return await ${entityName}API.getFormData(String(data.row.id));
});
}
};
// 处理工具栏按钮点击(删除等)
const handleToolbarClick = (name: string) => {
console.log(name);
};
</script>

View File

@@ -0,0 +1,433 @@
<template>
<div class="page-container">
<el-card class="page-search" shadow="never">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInQuery == 1)
<el-form-item label="$fieldConfig.fieldComment" prop="$fieldConfig.fieldName">
#if($fieldConfig.formType.name() == "INPUT")
<el-input
v-model="queryParams.$fieldConfig.fieldName"
placeholder="$fieldConfig.fieldComment"
clearable
@keyup.enter="handleQuery()"
/>
#elseif($fieldConfig.formType.name() == "SELECT")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<DictSelect v-model="queryParams.$fieldConfig.fieldName" code="$fieldConfig.dictType" />
#else
<el-select v-model="queryParams.$fieldConfig.fieldName" placeholder="请选择$fieldConfig.fieldComment">
<el-option :key="1" :value="1" label="选项一"/>
<el-option :key="2" :value="2" label="选项二"/>
</el-select>
#end
#elseif($fieldConfig.formType.name() == "RADIO")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<DictSelect v-model="queryParams.$fieldConfig.fieldName" type="radio" code="$fieldConfig.dictType" />
#else
<el-radio-group v-model="queryParams.$fieldConfig.fieldName">
<el-radio :key="1" :value="1">选项一</el-radio>
<el-radio :key="2" :value="2">选项二</el-radio>
</el-radio-group>
#end
#elseif($fieldConfig.formType.name() == "CHECK_BOX")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<DictSelect v-model="queryParams.$fieldConfig.fieldName" type="checkbox" code="$fieldConfig.dictType" />
#else
<el-checkbox-group v-model="queryParams.$fieldConfig.fieldName">
<el-checkbox :key="1" :label="1">选项一</el-checkbox>
<el-checkbox :key="2" :label="2">选项二</el-checkbox>
</el-checkbox-group>
#end
#elseif($fieldConfig.formType.name() == "INPUT_NUMBER")
<el-input-number
v-model="queryParams.$fieldConfig.fieldName"
placeholder="$fieldConfig.fieldComment"
/>
#elseif($fieldConfig.formType.name() == "SWITCH")
<el-switch
v-model="queryParams.$fieldConfig.fieldName"
:active-value="1"
:inactive-value="0"
/>
#elseif($fieldConfig.formType.name() == "TEXT_AREA")
<el-input type="textarea"
v-model="queryParams.$fieldConfig.fieldName"
placeholder="$fieldConfig.fieldComment"
/>
#elseif($fieldConfig.formType.name() == "DATE_TIME")
<el-date-picker
v-model="queryParams.$fieldConfig.fieldName"
#if($fieldConfig.queryType.name() == "BETWEEN")
type="daterange"
range-separator="~"
start-placeholder="开始时间"
end-placeholder="结束时间"
#else
type="datetime"
placeholder="$fieldConfig.fieldComment"
#end
value-format="YYYY-MM-DD HH:mm:ss"
/>
#elseif($fieldConfig.formType.name() == "DATE")
<el-date-picker
class="!w-[240px]"
v-model="queryParams.$fieldConfig.fieldName"
#if($fieldConfig.queryType.name() == "BETWEEN")
type="daterange"
range-separator="~"
start-placeholder="开始时间"
end-placeholder="结束时间"
#else
type="date"
placeholder="$fieldConfig.fieldComment"
#end
value-format="YYYY-MM-DD"
/>
#end
</el-form-item>
#end
#end
<el-form-item>
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="page-content" shadow="never">
<div class="page-toolbar">
<div class="page-toolbar__left">
<el-button
v-hasPerm="['${moduleName}:${entityKebab}:create']"
type="success"
icon="plus"
@click="handleCreateClick()"
>新增</el-button>
<el-button
v-hasPerm="['${moduleName}:${entityKebab}:delete']"
type="danger"
:disabled="!hasSelection"
icon="delete"
@click="handleDelete()"
>删除</el-button>
</div>
</div>
<el-table
ref="dataTableRef"
v-loading="loading"
:data="pageData"
border
stripe
highlight-current-row
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInList == 1)
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<el-table-column label="$fieldConfig.fieldComment" width="150" align="center">
<template #default="scope">
<DictTag v-model="scope.row.$fieldConfig.fieldName" code="$fieldConfig.dictType" />
</template>
</el-table-column>
#else
<el-table-column
key="$fieldConfig.fieldName"
label="$fieldConfig.fieldComment"
prop="$fieldConfig.fieldName"
min-width="150"
align="center"
/>
#end
#end
#end
<el-table-column fixed="right" label="操作" width="220">
<template #default="scope">
<el-button
v-hasPerm="['${moduleName}:${entityKebab}:update']"
type="primary"
size="small"
link
icon="edit"
@click="handleEditClick(String(scope.row.id))"
>
编辑
</el-button>
<el-button
v-hasPerm="['${moduleName}:${entityKebab}:delete']"
type="danger"
size="small"
link
icon="delete"
@click="handleDelete(String(scope.row.id))"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="page-pagination">
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchList"
/>
</div>
</el-card>
<!-- $!{businessName}表单弹窗 -->
<el-dialog
v-model="dialog.visible"
:title="dialog.title"
width="500px"
@close="closeDialog"
>
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-width="100px">
#foreach($fieldConfig in $fieldConfigs)
#if($fieldConfig.isShowInForm == 1 && $fieldConfig.formType.name() != "HIDDEN")
<el-form-item label="$fieldConfig.fieldComment" prop="$fieldConfig.fieldName">
#if($fieldConfig.formType.name() == "INPUT")
<el-input
v-model="formData.$fieldConfig.fieldName"
placeholder="$fieldConfig.fieldComment"
/>
#elseif($fieldConfig.formType.name() == "SELECT")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<DictSelect v-model="formData.$fieldConfig.fieldName" code="$fieldConfig.dictType" />
#else
<el-select v-model="formData.$fieldConfig.fieldName" placeholder="请选择$fieldConfig.fieldComment">
<el-option :value="0" label="选项一"/>
<el-option :value="1" label="选项二"/>
</el-select>
#end
#elseif($fieldConfig.formType.name() == "RADIO")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<DictSelect v-model="formData.$fieldConfig.fieldName" type="radio" code="$fieldConfig.dictType" />
#else
<el-radio-group v-model="formData.$fieldConfig.fieldName">
<el-radio :value="0">选项一</el-radio>
<el-radio :value="1">选项二</el-radio>
</el-radio-group>
#end
#elseif($fieldConfig.formType.name() == "CHECK_BOX")
#if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "")
<DictSelect v-model="formData.$fieldConfig.fieldName" type="checkbox" code="$fieldConfig.dictType" />
#else
<el-checkbox-group v-model="formData.$fieldConfig.fieldName">
<el-checkbox :value="0">选项一</el-checkbox>
<el-checkbox :value="1">选项二</el-checkbox>
</el-checkbox-group>
#end
#elseif($fieldConfig.formType.name() == "INPUT_NUMBER")
<el-input-number
v-model="formData.$fieldConfig.fieldName"
placeholder="$fieldConfig.fieldComment"
/>
#elseif($fieldConfig.formType.name() == "SWITCH")
<el-switch
v-model="formData.$fieldConfig.fieldName"
:active-value="1"
:inactive-value="0"
/>
#elseif($fieldConfig.formType.name() == "TEXT_AREA")
<el-input type="textarea"
v-model="formData.$fieldConfig.fieldName"
placeholder="$fieldConfig.fieldComment"
/>
#elseif($fieldConfig.formType.name() == "DATE_TIME")
<el-date-picker
v-model="formData.$fieldConfig.fieldName"
type="datetime"
placeholder="$fieldConfig.fieldComment"
value-format="YYYY-MM-DD HH:mm:ss"
/>
#elseif($fieldConfig.formType.name() == "DATE")
<el-date-picker
class="!w-[240px]"
v-model="formData.$fieldConfig.fieldName"
type="date"
placeholder="$fieldConfig.fieldComment"
value-format="YYYY-MM-DD"
/>
#end
</el-form-item>
#elseif($fieldConfig.formType.name() == "HIDDEN")
<el-input type="hidden" v-model="formData.$fieldConfig.fieldName" />
#end
#end
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit()">确定</el-button>
<el-button @click="closeDialog()">取消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
import { useTableSelection } from "@/composables";
import ${entityName}API from "@/api/${moduleName}/${entityKebab}";
import type { ${entityName}Item, ${entityName}Form, ${entityName}QueryParams } from "@/api/${moduleName}/${entityKebab}";
defineOptions({
name: "${entityName}",
inheritAttrs: false,
});
const queryFormRef = ref<FormInstance>();
const dataFormRef = ref<FormInstance>();
const loading = ref(false);
const total = ref(0);
const queryParams = reactive<${entityName}QueryParams>({
pageNum: 1,
pageSize: 10,
} as ${entityName}QueryParams);
// $!{businessName}表格数据
const pageData = ref<${entityName}Item[]>([]);
// 弹窗
const dialog = reactive({
title: "",
visible: false,
});
// $!{businessName}表单数据
const formData = reactive<${entityName}Form>({} as ${entityName}Form);
// $!{businessName}表单校验规则
const rules: FormRules = {
#if($fieldConfigs)
#foreach($fieldConfig in ${fieldConfigs})
#if($fieldConfig.isShowInForm && $fieldConfig.isRequired)
${fieldConfig.fieldName}: [{ required: true, message: "请输入${fieldConfig.fieldComment}", trigger: "blur" }],
#end
#end
#end
};
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<${entityName}Item>();
async function fetchList(): Promise<void> {
loading.value = true;
try {
const data = await ${entityName}API.getPage(queryParams);
pageData.value = data.list;
total.value = data.total ?? 0;
} finally {
loading.value = false;
}
}
/** 查询$!{businessName} */
function handleQuery(): void {
queryParams.pageNum = 1;
fetchList();
}
/** 重置$!{businessName}查询 */
function handleResetQuery() {
queryFormRef.value?.resetFields();
handleQuery();
}
function openDialog(): void {
dialog.visible = true;
}
function closeDialog(): void {
dialog.visible = false;
resetForm();
}
function resetForm(): void {
dataFormRef.value?.resetFields();
dataFormRef.value?.clearValidate();
formData.id = undefined;
}
async function handleCreateClick(): Promise<void> {
dialog.title = "新增$!{businessName}";
openDialog();
}
async function handleEditClick(id: string): Promise<void> {
dialog.title = "修改$!{businessName}";
${entityName}API.getFormData(id).then((data) => {
Object.assign(formData, data);
openDialog();
});
}
/** 提交$!{businessName}表单 */
function handleSubmit() {
dataFormRef.value?.validate((valid: any) => {
if (valid) {
loading.value = true;
const id = formData.id;
if (id) {
${entityName}API.update(id, formData)
.then(() => {
ElMessage.success("修改成功");
closeDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
} else {
${entityName}API.create(formData)
.then(() => {
ElMessage.success("新增成功");
closeDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
}
}
});
}
/** 删除$!{businessName} */
function handleDelete(id?: string) {
const ids = [id || selectedIds.value].join(",");
if (!ids) {
ElMessage.warning("请勾选删除项");
return;
}
ElMessageBox.confirm("确认删除已选中的数据项?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(
() => {
loading.value = true;
${entityName}API.deleteByIds(ids)
.then(() => {
ElMessage.success("删除成功");
handleResetQuery();
})
.finally(() => (loading.value = false));
},
() => {
ElMessage.info("已取消删除");
}
);
}
onMounted(() => {
handleQuery();
});
</script>