diff --git a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/constant/GenConstants.java b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/constant/GenConstants.java index c79f02cc8..ddf49b892 100644 --- a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/constant/GenConstants.java +++ b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/constant/GenConstants.java @@ -80,6 +80,11 @@ public interface GenConstants { */ String SORT_FIELD = "sortField"; + /** + * 默认前端模板类型,对应 vm/vue 目录。 + */ + String FRONTEND_TYPE_VUE = "vue"; + /** * 树根节点值 */ @@ -267,12 +272,11 @@ public interface GenConstants { String JAVA_CONTROLLER_TEMPLATE_PATH = "vm/java/controller.java.vm"; // MyBatis MapperXML 模板 String XML_MAPPER_TEMPLATE_PATH = "vm/xml/mapper.xml.vm"; - // 前端接口源码模板 - String TS_API_TEMPLATE_PATH = "vm/ts/api.ts.vm"; - String TS_TYPES_TEMPLATE_PATH = "vm/ts/types.ts.vm"; - // 前端页面源码模板 - String VUE_INDEX_TEMPLATE_PATH = "vm/vue/index.vue.vm"; - String VUE_INDEX_TREE_TEMPLATE_PATH = "vm/vue/index-tree.vue.vm"; + // 前端源码模板 + String FRONTEND_API_TEMPLATE_NAME = "api.ts.vm"; + String FRONTEND_TYPES_TEMPLATE_NAME = "types.ts.vm"; + String FRONTEND_INDEX_TEMPLATE_PREFIX = "index"; + String FRONTEND_INDEX_TREE_TEMPLATE_PREFIX = "index-tree"; // 数据库SQL模板 String SQL_ORACLE_TEMPLATE_PATH = "vm/sql/oracle.sql.vm"; String SQL_POSTGRES_TEMPLATE_PATH = "vm/sql/postgres.sql.vm"; @@ -280,7 +284,7 @@ public interface GenConstants { String SQL_MYSQL_TEMPLATE_PATH = "vm/sql/mysql.sql.vm"; /** - * 所有模板路径集合 + * 固定模板路径集合,前端模板按 frontendType 动态解析。 */ Set TEMPLATE_PATHS = CollUtil.newHashSet( JAVA_DOMAIN_TEMPLATE_PATH @@ -291,10 +295,6 @@ public interface GenConstants { , JAVA_SERVICE_IMPL_TEMPLATE_PATH , JAVA_CONTROLLER_TEMPLATE_PATH , XML_MAPPER_TEMPLATE_PATH - , TS_API_TEMPLATE_PATH - , TS_TYPES_TEMPLATE_PATH - , VUE_INDEX_TEMPLATE_PATH - , VUE_INDEX_TREE_TEMPLATE_PATH , SQL_ORACLE_TEMPLATE_PATH , SQL_POSTGRES_TEMPLATE_PATH , SQL_SQLSERVER_TEMPLATE_PATH diff --git a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/domain/GenTable.java b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/domain/GenTable.java index 0fe6b0c99..51555b038 100644 --- a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/domain/GenTable.java +++ b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/domain/GenTable.java @@ -1,16 +1,15 @@ package org.dromara.gen.domain; -import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.gen.constant.GenConstants; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.EqualsAndHashCode; -import org.dromara.common.core.utils.StringUtils; -import org.dromara.common.mybatis.core.domain.BaseEntity; -import org.dromara.gen.constant.GenConstants; import java.util.HashMap; import java.util.List; @@ -62,6 +61,11 @@ public class GenTable extends BaseEntity { */ private String tplCategory; + /** + * 前端模板类型,对应 vm 下的模板目录 + */ + private String frontendType; + /** * 生成包路径 */ diff --git a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/service/GenTableServiceImpl.java b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/service/GenTableServiceImpl.java index ebce0011f..5482a9836 100644 --- a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/service/GenTableServiceImpl.java +++ b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/service/GenTableServiceImpl.java @@ -8,7 +8,6 @@ import cn.hutool.core.lang.Dict; import cn.hutool.core.util.ObjectUtil; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.dynamic.datasource.annotation.DSTransactional; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -22,7 +21,6 @@ import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.StreamUtils; import org.dromara.common.core.utils.StringUtils; -import org.dromara.common.core.utils.file.FileUtils; import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.utils.IdGeneratorUtil; @@ -38,7 +36,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.*; @@ -55,7 +52,7 @@ import java.util.zip.ZipOutputStream; @Service public class GenTableServiceImpl implements IGenTableService { - private final GenTableMapper genTableMapper; + private final GenTableMapper tableMapper; private final GenTableColumnMapper genTableColumnMapper; private static final String[] TABLE_IGNORE = new String[]{"sj_", "flow_", "gen_"}; @@ -68,9 +65,10 @@ public class GenTableServiceImpl implements IGenTableService { */ @Override public List selectGenTableColumnListByTableId(Long tableId) { - return genTableColumnMapper.selectList(new LambdaQueryWrapper() + return genTableColumnMapper.lambda() .eq(GenTableColumn::getTableId, tableId) - .orderByAsc(GenTableColumn::getSort)); + .orderByAsc(GenTableColumn::getSort) + .list(); } /** @@ -95,7 +93,7 @@ public class GenTableServiceImpl implements IGenTableService { */ @Override public PageResult selectPageGenTableList(GenTable genTable, PageQuery pageQuery) { - Page page = genTableMapper.selectPage(pageQuery.build(), this.buildGenTableQueryWrapper(genTable)); + Page page = tableMapper.selectPage(pageQuery.build(), this.buildGenTableQueryWrapper(genTable)); return PageResult.build(page.getRecords(), page.getTotal()); } @@ -136,7 +134,7 @@ public class GenTableServiceImpl implements IGenTableService { if (CollUtil.isEmpty(tablesMap)) { return PageResult.build(); } - List tableNames = genTableMapper.selectTableNameList(genTable.getDataName()); + List tableNames = tableMapper.selectTableNameList(genTable.getDataName()); String[] tableArrays; if (CollUtil.isNotEmpty(tableNames)) { tableArrays = tableNames.toArray(new String[0]); @@ -232,7 +230,7 @@ public class GenTableServiceImpl implements IGenTableService { normalizeColumnOptions(genTable.getColumns()); String options = JsonUtils.toJsonString(genTable.getParams()); genTable.setOptions(options); - int row = genTableMapper.updateById(genTable); + int row = tableMapper.updateById(genTable); if (row > 0) { genTableColumnMapper.updateBatchById(genTable.getColumns()); } @@ -247,8 +245,10 @@ public class GenTableServiceImpl implements IGenTableService { @Override public void deleteGenTableByIds(Long[] tableIds) { List ids = Arrays.asList(tableIds); - genTableMapper.deleteByIds(ids); - genTableColumnMapper.delete(new LambdaQueryWrapper().in(GenTableColumn::getTableId, ids)); + tableMapper.deleteByIds(ids); + genTableColumnMapper.lambda() + .in(GenTableColumn::getTableId, ids) + .deleteCount(); } /** @@ -265,7 +265,7 @@ public class GenTableServiceImpl implements IGenTableService { String tableName = table.getTableName(); GenUtils.initTable(table); table.setDataName(dataName); - int row = genTableMapper.insert(table); + int row = tableMapper.insert(table); if (row > 0) { // 保存列信息 List genTableColumns = SpringUtils.getAopProxy(this).selectDbTableColumnsByName(tableName, dataName); @@ -453,10 +453,17 @@ public class GenTableServiceImpl implements IGenTableService { table.setMenuIds(menuIds); setPkColumn(table); Dict context = TemplateEngineUtils.buildContext(table); - List templates = TemplateEngineUtils.getTemplateList(table.getTplCategory(), table.getDataName()); + List templates = TemplateEngineUtils.getTemplateList(table.getTplCategory(), table.getDataName(), table.getFrontendType()); return new RenderContext(table, context, templates); } + /** + * 模板渲染上下文。 + * + * @param table 生成表信息 + * @param context 模板上下文 + * @param templates 待渲染模板 + */ private record RenderContext(GenTable table, Dict context, List templates) { } @@ -481,6 +488,11 @@ public class GenTableServiceImpl implements IGenTableService { } } + /** + * 校验生成选项中配置的字段是否存在。 + * + * @param genTable 业务表信息 + */ private void validateOptionColumns(GenTable genTable) { Map params = genTable.getParams(); if (CollUtil.isEmpty(params) || CollUtil.isEmpty(genTable.getColumns())) { @@ -503,6 +515,13 @@ public class GenTableServiceImpl implements IGenTableService { } } + /** + * 校验单个选项字段。 + * + * @param validFields 有效字段集合 + * @param field 待校验字段 + * @param label 字段显示名称 + */ private void validateOptionField(Set validFields, Object field, String label) { if (ObjectUtil.isNull(field)) { return; @@ -516,6 +535,11 @@ public class GenTableServiceImpl implements IGenTableService { } } + /** + * 规范化字段扩展配置。 + * + * @param columns 表字段列表 + */ private void normalizeColumnOptions(List columns) { if (CollUtil.isEmpty(columns)) { return; @@ -534,7 +558,7 @@ public class GenTableServiceImpl implements IGenTableService { * @return 包含字段集合的业务表实体 */ private GenTable getGenTable(Long tableId) { - GenTable table = genTableMapper.selectById(tableId); + GenTable table = tableMapper.selectById(tableId); if (ObjectUtil.isNull(table)) { throw new ServiceException("业务表不存在"); } @@ -553,10 +577,11 @@ public class GenTableServiceImpl implements IGenTableService { return tables; } List tableIds = StreamUtils.toList(tables, GenTable::getTableId); - List columns = genTableColumnMapper.selectList(new LambdaQueryWrapper() + List columns = genTableColumnMapper.lambda() .in(GenTableColumn::getTableId, tableIds) .orderByAsc(GenTableColumn::getTableId) - .orderByAsc(GenTableColumn::getSort)); + .orderByAsc(GenTableColumn::getSort) + .list(); Map> columnMap = StreamUtils.groupByKey(columns, GenTableColumn::getTableId); tables.forEach(table -> table.setColumns(columnMap.getOrDefault(table.getTableId(), new ArrayList<>()))); return tables; @@ -626,4 +651,3 @@ public class GenTableServiceImpl implements IGenTableService { } } - diff --git a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/GenUtils.java b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/GenUtils.java index b82ba1e55..6d5bf98bd 100644 --- a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/GenUtils.java +++ b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/GenUtils.java @@ -34,6 +34,7 @@ public class GenUtils { genTable.setBusinessName(getBusinessName(genTable.getTableName())); genTable.setFunctionName(replaceText(genTable.getTableComment())); genTable.setFunctionAuthor(PROPERTIES.getAuthor()); + genTable.setFrontendType(GenConstants.FRONTEND_TYPE_VUE); genTable.setCreateTime(null); genTable.setUpdateTime(null); } @@ -134,6 +135,15 @@ public class GenUtils { } } + /** + * 解析数值类型对应的 Java 类型。 + * + * @param dataType 数据库字段类型 + * @param columnLength 字段长度 + * @param columnScale 小数位数 + * @param columnName 字段名称 + * @return Java 类型 + */ private static String resolveNumberJavaType(String dataType, Integer columnLength, Integer columnScale, String columnName) { if (isBooleanColumn(dataType, columnLength, columnScale, columnName)) { return GenConstants.TYPE_BOOLEAN; @@ -159,6 +169,12 @@ public class GenUtils { return GenConstants.TYPE_LONG; } + /** + * 根据整数字段长度解析 Java 类型。 + * + * @param columnLength 字段长度 + * @return Java 类型 + */ private static String resolveIntegerJavaType(Integer columnLength) { if (columnLength > 0 && columnLength <= 9) { return GenConstants.TYPE_INTEGER; @@ -166,6 +182,15 @@ public class GenUtils { return GenConstants.TYPE_LONG; } + /** + * 判断字段是否适合按布尔类型生成。 + * + * @param dataType 数据库字段类型 + * @param columnLength 字段长度 + * @param columnScale 小数位数 + * @param columnName 字段名称 + * @return 是否布尔字段 + */ private static boolean isBooleanColumn(String dataType, Integer columnLength, Integer columnScale, String columnName) { if (columnScale > 0) { return false; @@ -180,6 +205,12 @@ public class GenUtils { return false; } + /** + * 判断字段名称是否为开关类字段。 + * + * @param columnName 字段名称 + * @return 是否开关类字段 + */ private static boolean isSwitchColumn(String columnName) { return StringUtils.endsWithAny(columnName, "status", "flag", "enabled", "disabled", "available", "visible") || columnName.startsWith("is_") @@ -188,6 +219,12 @@ public class GenUtils { || columnName.startsWith("disable_"); } + /** + * 判断字段名称是否为排序字段。 + * + * @param columnName 字段名称 + * @return 是否排序字段 + */ private static boolean isSortColumn(String columnName) { return StringUtils.endsWithAny(columnName, "sort", "order_num", "order", "rank", "seq", "sequence"); } diff --git a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/TemplateEngineUtils.java b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/TemplateEngineUtils.java index edf7ad00a..1dd10fe89 100644 --- a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/TemplateEngineUtils.java +++ b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/TemplateEngineUtils.java @@ -9,6 +9,7 @@ import cn.hutool.extra.template.TemplateUtil; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.DateUtils; import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.StringUtils; @@ -20,12 +21,14 @@ import org.dromara.gen.constant.GenConstants; import org.dromara.gen.domain.GenTable; import org.dromara.gen.domain.GenTableColumn; import org.dromara.gen.util.template.PathNamedTemplate; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import java.io.IOException; import java.util.*; import java.util.function.Consumer; -import static org.dromara.gen.constant.GenConstants.TS_TYPES_TEMPLATE_PATH; - /** * 模板引擎工具 */ @@ -51,6 +54,7 @@ public class TemplateEngineUtils { // 模板引擎 private static final TemplateEngine TEMPLATE_ENGINE; private static final Map TEMPLATE_MAPPER; + private static final ResourcePatternResolver RESOURCE_PATTERN_RESOLVER = new PathMatchingResourcePatternResolver(); static { // 模板引擎初始化 @@ -87,6 +91,7 @@ public class TemplateEngineUtils { String functionName = genTable.getFunctionName(); context.put("tplCategory", genTable.getTplCategory()); + context.put("frontendType", resolveFrontendType(genTable.getFrontendType())); context.put("tableName", genTable.getTableName()); context.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】"); context.put("ClassName", genTable.getClassName()); @@ -140,9 +145,9 @@ public class TemplateEngineUtils { /** * 向树形模板上下文写入树字段相关变量。 * - * @param context 模板上下文 - * @param genTable 代码生成业务表对象 - * @param paramsObj 已解析的 options 参数(避免重复解析) + * @param context 模板上下文 + * @param genTable 代码生成业务表对象 + * @param paramsObj 已解析的 options 参数(避免重复解析) */ public static void setTreeContext(Dict context, GenTable genTable, Dict paramsObj) { String treeCode = getTreeCode(paramsObj); @@ -188,6 +193,18 @@ public class TemplateEngineUtils { * @return 模板列表 */ public static List getTemplateList(String tplCategory, String dsName) { + return getTemplateList(tplCategory, dsName, GenConstants.FRONTEND_TYPE_VUE); + } + + /** + * 获取模板信息 + * + * @param tplCategory 前端页面模板分类 + * @param dsName 数据源名称 + * @param frontendType 前端模板类型 + * @return 模板列表 + */ + public static List getTemplateList(String tplCategory, String dsName, String frontendType) { List templates = new ArrayList<>(); // 后端源码模板 templates.add(TEMPLATE_MAPPER.get(GenConstants.JAVA_DOMAIN_TEMPLATE_PATH)); @@ -199,9 +216,9 @@ public class TemplateEngineUtils { templates.add(TEMPLATE_MAPPER.get(GenConstants.JAVA_CONTROLLER_TEMPLATE_PATH)); // MyBatis MapperXML 模板 templates.add(TEMPLATE_MAPPER.get(GenConstants.XML_MAPPER_TEMPLATE_PATH)); - // 前端接口源码模板 - templates.add(TEMPLATE_MAPPER.get(GenConstants.TS_API_TEMPLATE_PATH)); - templates.add(TEMPLATE_MAPPER.get(TS_TYPES_TEMPLATE_PATH)); + // 前端 API 与类型模板 + templates.add(getTemplate(getFrontendApiTemplatePath(frontendType))); + templates.add(getTemplate(getFrontendTypesTemplatePath(frontendType))); // 数据库模板 DataBaseType dataBaseType = DataBaseHelper.getDataBaseType(dsName); if (dataBaseType.isOracle()) { @@ -214,15 +231,29 @@ public class TemplateEngineUtils { // 默认使用MySQL模板 templates.add(TEMPLATE_MAPPER.get(GenConstants.SQL_MYSQL_TEMPLATE_PATH)); } - // 前端页面源码模板 + // 前端页面模板 if (GenConstants.TPL_CRUD.equals(tplCategory)) { - templates.add(TEMPLATE_MAPPER.get(GenConstants.VUE_INDEX_TEMPLATE_PATH)); + templates.add(getTemplate(getFrontendIndexTemplatePath(frontendType))); } else if (GenConstants.TPL_TREE.equals(tplCategory)) { - templates.add(TEMPLATE_MAPPER.get(GenConstants.VUE_INDEX_TREE_TEMPLATE_PATH)); + templates.add(getTemplate(getFrontendIndexTreeTemplatePath(frontendType))); } return templates; } + /** + * 获取模板,支持按前端模板类型动态加载 vm/{frontendType} 下的模板。 + * + * @param templatePath 模板路径 + * @return 带路径名的模板 + */ + private static PathNamedTemplate getTemplate(String templatePath) { + PathNamedTemplate template = TEMPLATE_MAPPER.get(templatePath); + if (ObjectUtil.isNotNull(template)) { + return template; + } + return PathNamedTemplate.form(TEMPLATE_ENGINE, templatePath); + } + /** * 获取文件名 * @@ -244,7 +275,7 @@ public class TemplateEngineUtils { String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/"); String mybatisPath = MYBATIS_PATH + "/" + moduleName; - String vuePath = "vue"; + String frontendPath = getFrontendPath(genTable.getFrontendType()); // templatePath // genFilePathFormat if (template.contains("domain.java.vm")) { @@ -266,17 +297,112 @@ public class TemplateEngineUtils { } else if (template.contains("sql.vm")) { fileName = businessName + "Menu.sql"; } else if (template.contains("api.ts.vm")) { - fileName = StringUtils.format("{}/api/{}/{}/index.ts", vuePath, moduleName, businessName); + fileName = StringUtils.format("{}/api/{}/{}/index.ts", frontendPath, moduleName, businessName); } else if (template.contains("types.ts.vm")) { - fileName = StringUtils.format("{}/api/{}/{}/types.ts", vuePath, moduleName, businessName); - } else if (template.contains("index.vue.vm")) { - fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); - } else if (template.contains("index-tree.vue.vm")) { - fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + fileName = StringUtils.format("{}/api/{}/{}/types.ts", frontendPath, moduleName, businessName); + } else if (isFrontendPageTemplate(template, GenConstants.FRONTEND_INDEX_TEMPLATE_PREFIX)) { + fileName = StringUtils.format("{}/views/{}/{}/index.{}", frontendPath, moduleName, businessName, + getFrontendPageExtension(template, GenConstants.FRONTEND_INDEX_TEMPLATE_PREFIX)); + } else if (isFrontendPageTemplate(template, GenConstants.FRONTEND_INDEX_TREE_TEMPLATE_PREFIX)) { + fileName = StringUtils.format("{}/views/{}/{}/index.{}", frontendPath, moduleName, businessName, + getFrontendPageExtension(template, GenConstants.FRONTEND_INDEX_TREE_TEMPLATE_PREFIX)); } return fileName; } + /** + * 获取前端输出目录。 + * + * @param frontendType 前端模板类型 + * @return 前端输出目录 + */ + private static String getFrontendPath(String frontendType) { + return resolveFrontendType(frontendType); + } + + /** + * 获取前端页面文件扩展名。 + * + * @param template 模板路径 + * @param templatePrefix 模板文件名前缀 + * @return 文件扩展名 + */ + private static String getFrontendPageExtension(String template, String templatePrefix) { + String fileName = getTemplateFileName(template); + return fileName.substring((templatePrefix + ".").length(), fileName.length() - ".vm".length()); + } + + private static String getFrontendApiTemplatePath(String frontendType) { + return getFrontendTemplatePath(frontendType, GenConstants.FRONTEND_API_TEMPLATE_NAME); + } + + private static String getFrontendTypesTemplatePath(String frontendType) { + return getFrontendTemplatePath(frontendType, GenConstants.FRONTEND_TYPES_TEMPLATE_NAME); + } + + private static String getFrontendIndexTemplatePath(String frontendType) { + return getFrontendPageTemplatePath(frontendType, GenConstants.FRONTEND_INDEX_TEMPLATE_PREFIX); + } + + private static String getFrontendIndexTreeTemplatePath(String frontendType) { + return getFrontendPageTemplatePath(frontendType, GenConstants.FRONTEND_INDEX_TREE_TEMPLATE_PREFIX); + } + + /** + * 解析前端模板类型。 + * + * @param frontendType 前端模板类型 + * @return 已规范化的模板目录名 + */ + private static String resolveFrontendType(String frontendType) { + String type = StringUtils.blankToDefault(frontendType, GenConstants.FRONTEND_TYPE_VUE); + if (!type.matches("[A-Za-z0-9_-]+")) { + throw new ServiceException("前端模板类型仅支持字母、数字、下划线和中划线"); + } + return type; + } + + private static String getFrontendTemplatePath(String frontendType, String templateName) { + return StringUtils.format("vm/{}/{}", resolveFrontendType(frontendType), templateName); + } + + /** + * 获取前端页面模板路径,页面输出扩展名由模板文件名决定。 + * + * @param frontendType 前端模板类型 + * @param templatePrefix 页面模板文件名前缀 + * @return 页面模板路径 + */ + private static String getFrontendPageTemplatePath(String frontendType, String templatePrefix) { + String type = resolveFrontendType(frontendType); + String pattern = StringUtils.format("classpath*:vm/{}/{}.*.vm", type, templatePrefix); + try { + Resource[] resources = RESOURCE_PATTERN_RESOLVER.getResources(pattern); + if (resources.length == 0) { + throw new ServiceException(StringUtils.format("未找到前端模板: vm/{}/{}.*.vm", type, templatePrefix)); + } + return Arrays.stream(resources) + .map(Resource::getFilename) + .filter(StringUtils::isNotBlank) + .sorted() + .findFirst() + .map(fileName -> StringUtils.format("vm/{}/{}", type, fileName)) + .orElseThrow(() -> new ServiceException(StringUtils.format("未找到前端模板: vm/{}/{}.*.vm", type, templatePrefix))); + } catch (IOException e) { + throw new ServiceException(StringUtils.format("读取前端模板失败: vm/{}/{}.*.vm", type, templatePrefix), e); + } + } + + private static boolean isFrontendPageTemplate(String template, String templatePrefix) { + String fileName = getTemplateFileName(template); + return fileName.startsWith(templatePrefix + ".") && fileName.endsWith(".vm"); + } + + private static String getTemplateFileName(String template) { + int index = Math.max(template.lastIndexOf('/'), template.lastIndexOf('\\')); + return index >= 0 ? template.substring(index + 1) : template; + } + /** * 获取包前缀 * @@ -328,14 +454,14 @@ public class TemplateEngineUtils { /** * 添加字典列表 * - * @param dicts 字典列表 + * @param dicts 字典列表 * @param columns 列集合 */ public static void addDicts(Set dicts, List columns) { for (GenTableColumn column : columns) { if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType()) && StringUtils.equalsAny( column.getHtmlType(), - new String[] { GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX, GenConstants.HTML_SWITCH })) { + GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX, GenConstants.HTML_SWITCH)) { dicts.add("'" + column.getDictType() + "'"); } } @@ -408,7 +534,7 @@ public class TemplateEngineUtils { /** * 获取树根节点值。 * - * @param paramsObj 其他选项 + * @param paramsObj 其他选项 * @param treeParentColumn 父节点字段 * @return 树根节点值 */ @@ -423,6 +549,14 @@ public class TemplateEngineUtils { return defaultValue; } + /** + * 读取布尔类型生成选项。 + * + * @param paramsObj 生成选项 + * @param key 选项键 + * @param defaultValue 默认值 + * @return 选项值 + */ private static boolean getBooleanOption(Dict paramsObj, String key, boolean defaultValue) { if (CollUtil.isEmpty(paramsObj) || !paramsObj.containsKey(key)) { return defaultValue; @@ -430,6 +564,13 @@ public class TemplateEngineUtils { return Convert.toBool(paramsObj.get(key), defaultValue); } + /** + * 根据字段名查找业务表字段。 + * + * @param genTable 业务表 + * @param field 字段名称或 Java 属性名 + * @return 业务表字段 + */ private static GenTableColumn getColumn(GenTable genTable, String field) { if (StringUtils.isBlank(field) || CollUtil.isEmpty(genTable.getColumns())) { return null; @@ -442,6 +583,13 @@ public class TemplateEngineUtils { return null; } + /** + * 根据字段配置查找业务表字段列表。 + * + * @param genTable 业务表 + * @param fieldValues 字段配置值 + * @return 业务表字段列表 + */ private static List getColumns(GenTable genTable, Object fieldValues) { List fields = new ArrayList<>(); if (fieldValues instanceof Collection collection) { @@ -459,6 +607,13 @@ public class TemplateEngineUtils { return columns; } + /** + * 转换为 Java 字面量。 + * + * @param column 业务表字段 + * @param value 字段值 + * @return Java 字面量 + */ private static String getJavaLiteral(GenTableColumn column, String value) { if (ObjectUtil.isNull(column) || StringUtils.isBlank(value)) { return "null"; @@ -472,6 +627,13 @@ public class TemplateEngineUtils { return "\"" + value + "\""; } + /** + * 转换为 TypeScript 字面量。 + * + * @param column 业务表字段 + * @param value 字段值 + * @return TypeScript 字面量 + */ private static String getTsLiteral(GenTableColumn column, String value) { if (ObjectUtil.isNull(column) || StringUtils.isBlank(value)) { return "undefined"; diff --git a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/template/PathNamedTemplate.java b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/template/PathNamedTemplate.java index ded95dce8..43d041530 100644 --- a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/template/PathNamedTemplate.java +++ b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/template/PathNamedTemplate.java @@ -23,26 +23,56 @@ public class PathNamedTemplate implements Template { private final Template delegate; + /** + * 创建基于路径命名的模板。 + * + * @param pathName 路径名称 + * @param delegate 委托模板 + */ private PathNamedTemplate(String pathName, Template delegate) { this.pathName = pathName; this.delegate = delegate; } + /** + * 渲染模板到字符输出器。 + * + * @param bindingMap 模板绑定数据 + * @param writer 字符输出器 + */ @Override public void render(Map bindingMap, Writer writer) { delegate.render(bindingMap, writer); } + /** + * 渲染模板到输出流。 + * + * @param bindingMap 模板绑定数据 + * @param out 输出流 + */ @Override public void render(Map bindingMap, OutputStream out) { delegate.render(bindingMap, out); } + /** + * 渲染模板到文件。 + * + * @param bindingMap 模板绑定数据 + * @param file 输出文件 + */ @Override public void render(Map bindingMap, File file) { delegate.render(bindingMap, file); } + /** + * 渲染模板为字符串。 + * + * @param bindingMap 模板绑定数据 + * @return 渲染结果 + */ @Override public String render(Map bindingMap) { return delegate.render(bindingMap); @@ -66,8 +96,8 @@ public class PathNamedTemplate implements Template { * @param pathName 路径名称 * @return 带路径名称的模板 */ - public static PathNamedTemplate form(TemplateEngine templateEngine,String pathName) { - return new PathNamedTemplate(pathName,templateEngine.getTemplate(pathName)); + public static PathNamedTemplate form(TemplateEngine templateEngine, String pathName) { + return new PathNamedTemplate(pathName, templateEngine.getTemplate(pathName)); } /** diff --git a/ruoyi-modules/ruoyi-gen/src/main/resources/vm/react/api.ts.vm b/ruoyi-modules/ruoyi-gen/src/main/resources/vm/react/api.ts.vm new file mode 100644 index 000000000..0ff896bd5 --- /dev/null +++ b/ruoyi-modules/ruoyi-gen/src/main/resources/vm/react/api.ts.vm @@ -0,0 +1,94 @@ +import type { #if(!$table.tree)PageResult, #end R } from '@/api/types'; +import request from '@/api/request'; +import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from './types'; + +/** + * 查询${functionName}列表 + */ +export function list${BusinessName}(query?: ${BusinessName}Query) { + return request#end>>({ + url: '/${moduleName}/${businessName}/list', + method: 'get', + params: query + }); +} + +/** + * 查询${functionName}详细 + */ +export function get${BusinessName}(${pkColumn.javaField}: string | number) { + return request>({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }); +} + +/** + * 新增${functionName} + */ +export function add${BusinessName}(data: ${BusinessName}Form) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'post', + data + }); +} + +/** + * 修改${functionName} + */ +export function update${BusinessName}(data: ${BusinessName}Form) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'put', + data + }); +} + +#if($enableStatus) +/** + * 修改${functionName}状态 + */ +export function change${BusinessName}Status( + ${pkColumn.javaField}: string | number, + ${statusField}: #if($statusColumn.javaType == 'Boolean')boolean#elseif($statusColumn.javaType == 'Integer' || $statusColumn.javaType == 'Long')number#else string#end +) { + return request({ + url: '/${moduleName}/${businessName}/changeStatus', + method: 'put', + data: { + ${pkColumn.javaField}, + ${statusField} + } + }); +} + +#end +#if($enableSort) +/** + * 调整${functionName}排序 + */ +export function update${BusinessName}Sort( + ${pkColumn.javaField}: string | number, + ${sortField}: #if($sortColumn.javaType == 'LocalDateTime' || $sortColumn.javaType == 'String')string#else number#end +) { + return request({ + url: '/${moduleName}/${businessName}/updateSort', + method: 'put', + data: { + ${pkColumn.javaField}, + ${sortField} + } + }); +} + +#end +/** + * 删除${functionName} + */ +export function del${BusinessName}(${pkColumn.javaField}: string | number | Array) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }); +} diff --git a/ruoyi-modules/ruoyi-gen/src/main/resources/vm/react/index-tree.tsx.vm b/ruoyi-modules/ruoyi-gen/src/main/resources/vm/react/index-tree.tsx.vm new file mode 100644 index 000000000..c3952c827 --- /dev/null +++ b/ruoyi-modules/ruoyi-gen/src/main/resources/vm/react/index-tree.tsx.vm @@ -0,0 +1,472 @@ +#set($needDict = $dicts != '') +#set($needImagePreview = false) +#set($needImageUpload = false) +#set($needFileUpload = false) +#set($needEditor = false) +#set($needCheckbox = false) +#set($needSelect = false) +#set($needTextArea = false) +#set($needDigit = false) +#set($needDateField = false) +#set($needSwitchField = false) +#foreach($column in $columns) +#if($column.list && $column.htmlType == "imageUpload") +#set($needImagePreview = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "imageUpload") +#set($needImageUpload = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "fileUpload") +#set($needFileUpload = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "editor") +#set($needEditor = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "checkbox") +#set($needCheckbox = true) +#end +#if(($column.insert || $column.edit || $column.query) && ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "switch")) +#set($needSelect = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "textarea") +#set($needTextArea = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "inputNumber") +#set($needDigit = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "datetime") +#set($needDateField = true) +#end +#if(($column.insert || $column.edit || $column.list) && $column.htmlType == "switch") +#set($needSwitchField = true) +#end +#end +#macro(comment $column)#set($idx=$column.columnComment.indexOf("("))#if($idx != -1)$column.columnComment.substring(0, $idx)#else$column.columnComment#end#end +import { DeleteOutlined#if($enableExport), DownloadOutlined#end, EditOutlined, PlusOutlined, SortAscendingOutlined } from '@ant-design/icons'; +import { + ModalForm, + PageContainer, +#if($needCheckbox) + ProFormCheckbox, +#end +#if($needDateField) + ProFormDateTimePicker, +#end +#if($needDigit) + ProFormDigit, +#end +#if($needSelect) + ProFormSelect, +#end +#if($needTextArea) + ProFormTextArea, +#end + ProFormText, + ProFormTreeSelect, + ProTable, + type ActionType, + type ProColumns +} from '@ant-design/pro-components'; +import { useBoolean } from 'ahooks'; +import { Button, Form, message#if($enableStatus || $needSwitchField), Switch#end#if($enableSort), InputNumber#end } from 'antd'; +import { useMemo, useRef, useState } from 'react'; +import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types'; +import { + add${BusinessName}, +#if($enableStatus) + change${BusinessName}Status, +#end + del${BusinessName}, + get${BusinessName}, + list${BusinessName}, +#if($enableSort) + update${BusinessName}Sort, +#end + update${BusinessName} +} from '@/api/${moduleName}/${businessName}'; +#if($needDict) +import DictTag from '@/components/common/DictTag'; +#end +#if($needFileUpload) +import FileUpload from '@/components/common/FileUpload'; +#end +#if($needImagePreview) +import ImagePreview from '@/components/common/ImagePreview'; +#end +#if($needImageUpload) +import ImageUpload from '@/components/common/ImageUpload'; +#end +#if($needEditor) +import RichTextEditor from '@/components/common/RichTextEditor'; +#end +import RowActions from '@/components/common/RowActions'; +#if($needDict) +import { useDict } from '@/hooks/useDict'; +#end +#if($enableExport) +import { useTableExport } from '@/hooks/useTableExport'; +#end +import { useTreeTableExpand } from '@/hooks/useTreeTableExpand'; +import { useUserStore } from '@/stores/userStore'; +#if($needDict) +import { dictOptions } from '@/utils/dict'; +#end +#if($enableStatus) +import { confirmAction } from '@/utils/modal'; +#end +import { hasPermi } from '@/utils/permission'; +import { filterTree#if($needDateField), formatDateTimeFields#end, handleTree#if($needDateField), toDayjsFields#end } from '@/utils/ruoyi'; + +const default${BusinessName}Form: ${BusinessName}Form = { + ${treeParentCode}: ${treeRootValueTsLiteral}, +#foreach($column in $columns) +#if(($column.insert || $column.edit) && !$column.pk && $column.htmlType == "checkbox") + ${column.javaField}: [], +#end +#end +}; + +interface ${BusinessName}SelectNode { + title: string; + value: string | number; + children?: ${BusinessName}SelectNode[]; +} + +function toTreeSelectData(nodes: ${BusinessName}VO[]): ${BusinessName}SelectNode[] { + return nodes.map(node => ({ + title: String(node.${treeName} || ''), + value: node.${treeCode}, + children: node.children ? toTreeSelectData(node.children) : undefined + })); +} + +#macro(switchActiveValue $column)#if($column.javaType == "Boolean")true#elseif($column.javaType == "Integer" || $column.javaType == "Long")0#else'0'#end#end +#macro(switchInactiveValue $column)#if($column.javaType == "Boolean")false#elseif($column.javaType == "Integer" || $column.javaType == "Long")1#else'1'#end#end +#if($enableStatus) +const ${statusField}ActiveValue = #if($statusColumn.javaType == "Boolean")true#elseif($statusColumn.javaType == "Integer" || $statusColumn.javaType == "Long")0#else'0'#end; +const ${statusField}InactiveValue = #if($statusColumn.javaType == "Boolean")false#elseif($statusColumn.javaType == "Integer" || $statusColumn.javaType == "Long")1#else'1'#end; + +#end +export default function ${BusinessName}Page() { + const actionRef = useRef(undefined); + const [form] = Form.useForm<${BusinessName}Form>(); + const userInfo = useUserStore(state => state.userInfo); +#if($needDict) + const dicts = useDict(${dicts}); +#end + const [treeOptions, setTreeOptions] = useState<${BusinessName}VO[]>([]); + const [tableRows, setTableRows] = useState<${BusinessName}VO[]>([]); + const { expandAll, expandedRowKeys, onExpandedRowsChange, syncExpandedRows, toggleExpandAll } = + useTreeTableExpand<${BusinessName}VO>(row => row.${treeCode}); + const [modalOpen, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false); + const [modalTitle, setModalTitle] = useState(''); +#if($enableExport) + const { updateExportParams, exportFile } = useTableExport(); +#end + + const canAdd = hasPermi(userInfo, ['${permissionPrefix}:add']); + const canEdit = hasPermi(userInfo, ['${permissionPrefix}:edit']); + const canRemove = hasPermi(userInfo, ['${permissionPrefix}:remove']); +#if($enableExport) + const canExport = hasPermi(userInfo, ['${permissionPrefix}:export']); +#end + const treeSelectData = useMemo( + () => [{ title: '顶级节点', value: ${treeRootValueTsLiteral}, children: toTreeSelectData(treeOptions) }], + [treeOptions] + ); + + const loadTreeOptions = async (excludeId?: string | number) => { + const res = await list${BusinessName}(); + const rows = handleTree<${BusinessName}VO>(res.data || [], '${treeCode}', '${treeParentCode}'); + setTreeOptions(excludeId ? filterTree(rows, node => node.${treeCode} !== excludeId) : rows); + }; + + const openAdd = async (row?: ${BusinessName}VO) => { + await loadTreeOptions(); + form.resetFields(); + form.setFieldsValue({ ...default${BusinessName}Form, ${treeParentCode}: row?.${treeCode} || ${treeRootValueTsLiteral} }); + setModalTitle('添加${functionName}'); + openModal(); + }; + + const openEdit = async (row: ${BusinessName}VO) => { + await loadTreeOptions(row.${treeCode}); + const res = await get${BusinessName}(row.${pkColumn.javaField}); + const data = #if($needDateField)toDayjsFields({ ...res.data }, [ +#foreach($column in $columns) +#if(($column.insert || $column.edit) && $column.htmlType == "datetime") + '${column.javaField}', +#end +#end + ])#else{ ...res.data }#end; +#foreach($column in $columns) +#if(($column.insert || $column.edit) && $column.htmlType == "checkbox") + if (typeof data.${column.javaField} === 'string') { + data.${column.javaField} = data.${column.javaField}.split(','); + } +#end +#end + form.resetFields(); + form.setFieldsValue(data); + setModalTitle('修改${functionName}'); + openModal(); + }; + + const submitForm = async (values: ${BusinessName}Form) => { + const submitValues = #if($needDateField)formatDateTimeFields({ ...values }, [ +#foreach($column in $columns) +#if(($column.insert || $column.edit) && $column.htmlType == "datetime") + '${column.javaField}', +#end +#end + ])#else{ ...values }#end; +#foreach($column in $columns) +#if(($column.insert || $column.edit) && $column.htmlType == "checkbox") + if (Array.isArray(submitValues.${column.javaField})) { + submitValues.${column.javaField} = submitValues.${column.javaField}.join(','); + } +#end +#end + submitValues.${pkColumn.javaField} ? await update${BusinessName}(submitValues) : await add${BusinessName}(submitValues); + message.success('操作成功'); + form.resetFields(); + actionRef.current?.reload(); + return true; + }; + + const remove = async (row: ${BusinessName}VO) => { + await del${BusinessName}(row.${pkColumn.javaField}); + message.success('删除成功'); + actionRef.current?.reload(); + }; + +#if($enableStatus) + const handleStatusChange = async (row: ${BusinessName}VO, checked: boolean) => { + const previousStatus = row.${statusField}; + const status = checked ? ${statusField}ActiveValue : ${statusField}InactiveValue; + const text = checked ? '启用' : '停用'; + try { + await confirmAction(`确认要"${text}"吗?`); + await change${BusinessName}Status(row.${pkColumn.javaField}, status); + message.success(`${text}成功`); + actionRef.current?.reload(); + } catch { + row.${statusField} = previousStatus; + actionRef.current?.reload(); + } + }; + +#end +#if($enableSort) + const handleSortChange = async (row: ${BusinessName}VO, value?: number | null) => { + await update${BusinessName}Sort(row.${pkColumn.javaField}, value || 0); + message.success('排序更新成功'); + actionRef.current?.reload(); + }; + +#end + const columns: ProColumns<${BusinessName}VO>[] = [ +#foreach($column in $columns) +#set($javaField=$column.javaField) +#set($dictType=$column.dictType) +#if($column.list && !$column.pk) +#if($enableStatus && $statusField == $javaField) + { + title: '#comment($column)', + dataIndex: '${javaField}', + valueType: 'select', + width: 100, +#if("" != $dictType) + fieldProps: { options: dictOptions(dicts.${dictType}) }, +#end + render: (_, row) => ( + handleStatusChange(row, checked)} + /> + ) + }, +#elseif($enableSort && $sortField == $javaField) + { + title: '#comment($column)', + dataIndex: '${javaField}', + search: false, + width: 130, + render: (_, row) => handleSortChange(row, value)} /> + }, +#elseif($column.htmlType == "datetime") + { title: '#comment($column)', dataIndex: '${javaField}', valueType: 'dateTime', search: #if($column.query)true#else false#end, width: 170 }, +#elseif($column.htmlType == "imageUpload") + { + title: '#comment($column)', + dataIndex: '${javaField}Url', + search: false, + width: 100, + render: (_, row) => + }, +#elseif($column.dictColumn) + { + title: '#comment($column)', + dataIndex: '${javaField}', + valueType: 'select', + fieldProps: { options: dictOptions(dicts.${dictType}) }, + render: (_, row) => + }, +#elseif($column.htmlType == "switch") + { + title: '#comment($column)', + dataIndex: '${javaField}', + valueType: 'select', + width: 100, + render: (_, row) => + }, +#else + { title: '#comment($column)', dataIndex: '${javaField}'#if(!$column.query), search: false#end }, +#end +#end +#end + { + title: '操作', + valueType: 'option', + width: 130, + fixed: 'right', + render: (_, row) => ( + , onClick: () => openEdit(row) }, + canAdd && { key: 'add', label: '新增', icon: , onClick: () => openAdd(row) }, + canRemove && { + key: 'delete', + label: '删除', + icon: , + danger: true, + confirm: `是否确认删除${functionName}编号为"#[[${row.]]#${pkColumn.javaField}#[[}]]#"的数据项?`, + onClick: () => remove(row) + } + ]} + /> + ) + } + ]; + + return ( + + + actionRef={actionRef} + rowKey="${treeCode}" + columns={columns} + scroll={{ x: 1000 }} + pagination={false} + search={{ labelWidth: 90 }} + expandable={{ + expandedRowKeys, + onExpandedRowsChange + }} + request={async params => { + const res = await list${BusinessName}(params); + const rows = handleTree<${BusinessName}VO>(res.data || [], '${treeCode}', '${treeParentCode}'); + setTableRows(rows); + syncExpandedRows(rows, expandAll); +#if($enableExport) + updateExportParams(params); +#end + return { data: rows, total: rows.length, success: true }; + }} + toolbar={{ title: '${functionName}列表' }} + toolBarRender={() => [ + canAdd && ( + + ), + #if($enableExport), + canExport && ( + + )#end + ]} + /> + + + title={modalTitle} + open={modalOpen} + width={560} + form={form} + layout="vertical" + initialValues={default${BusinessName}Form} + modalProps={{ destroyOnHidden: true, onCancel: closeModal }} + onOpenChange={open => !open && closeModal()} + onFinish={submitForm} + > + + ); +} diff --git a/ruoyi-modules/ruoyi-gen/src/main/resources/vm/react/index.tsx.vm b/ruoyi-modules/ruoyi-gen/src/main/resources/vm/react/index.tsx.vm new file mode 100644 index 000000000..eddfbfe78 --- /dev/null +++ b/ruoyi-modules/ruoyi-gen/src/main/resources/vm/react/index.tsx.vm @@ -0,0 +1,460 @@ +#set($needDict = $dicts != '') +#set($needDateRange = false) +#set($needImagePreview = false) +#set($needImageUpload = false) +#set($needFileUpload = false) +#set($needEditor = false) +#set($needCheckbox = false) +#set($needSelect = false) +#set($needTextArea = false) +#set($needDigit = false) +#set($needDateField = false) +#set($needSwitchField = false) +#foreach($column in $columns) +#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN") +#set($needDateRange = true) +#end +#if($column.list && $column.htmlType == "imageUpload") +#set($needImagePreview = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "imageUpload") +#set($needImageUpload = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "fileUpload") +#set($needFileUpload = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "editor") +#set($needEditor = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "checkbox") +#set($needCheckbox = true) +#end +#if(($column.insert || $column.edit || $column.query) && ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "switch")) +#set($needSelect = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "textarea") +#set($needTextArea = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "inputNumber") +#set($needDigit = true) +#end +#if(($column.insert || $column.edit) && $column.htmlType == "datetime") +#set($needDateField = true) +#end +#if(($column.insert || $column.edit || $column.list) && $column.htmlType == "switch") +#set($needSwitchField = true) +#end +#end +#macro(comment $column)#set($idx=$column.columnComment.indexOf("("))#if($idx != -1)$column.columnComment.substring(0, $idx)#else$column.columnComment#end#end +import { DeleteOutlined#if($enableExport), DownloadOutlined#end, EditOutlined, PlusOutlined } from '@ant-design/icons'; +import { + ModalForm, + PageContainer, +#if($needCheckbox) + ProFormCheckbox, +#end +#if($needDateField) + ProFormDateTimePicker, +#end +#if($needDigit) + ProFormDigit, +#end +#if($needSelect) + ProFormSelect, +#end +#if($needTextArea) + ProFormTextArea, +#end + ProFormText, + ProTable, + type ActionType, + type ProColumns +} from '@ant-design/pro-components'; +import { useBoolean } from 'ahooks'; +import { Button, Form, message, Popconfirm#if($enableStatus || $needSwitchField), Switch#end#if($enableSort), InputNumber#end } from 'antd'; +import { useRef, useState } from 'react'; +import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types'; +import { + add${BusinessName}, +#if($enableStatus) + change${BusinessName}Status, +#end + del${BusinessName}, + get${BusinessName}, + list${BusinessName}, +#if($enableSort) + update${BusinessName}Sort, +#end + update${BusinessName} +} from '@/api/${moduleName}/${businessName}'; +#if($needDict) +import DictTag from '@/components/common/DictTag'; +#end +#if($needFileUpload) +import FileUpload from '@/components/common/FileUpload'; +#end +#if($needImagePreview) +import ImagePreview from '@/components/common/ImagePreview'; +#end +#if($needImageUpload) +import ImageUpload from '@/components/common/ImageUpload'; +#end +#if($needEditor) +import RichTextEditor from '@/components/common/RichTextEditor'; +#end +import RowActions from '@/components/common/RowActions'; +#if($needDict) +import { useDict } from '@/hooks/useDict'; +#end +#if($needDateRange) +import { useDateRangeQuery } from '@/hooks/useDateRangeQuery'; +#end +#if($enableExport) +import { useTableExport } from '@/hooks/useTableExport'; +#end +import { useTableSelection } from '@/hooks/useTableSelection'; +import { useUserStore } from '@/stores/userStore'; +#if($needDict) +import { dictOptions } from '@/utils/dict'; +#end +#if($enableStatus) +import { confirmAction } from '@/utils/modal'; +#end +import { hasPermi } from '@/utils/permission'; +import { #if($needDateField)formatDateTimeFields, toDayjsFields, #end toPageQuery, toTableData } from '@/utils/ruoyi'; + +const default${BusinessName}Form: ${BusinessName}Form = { +#foreach($column in $columns) +#if(($column.insert || $column.edit) && !$column.pk && $column.htmlType == "checkbox") + ${column.javaField}: [], +#end +#end +}; + +#macro(switchActiveValue $column)#if($column.javaType == "Boolean")true#elseif($column.javaType == "Integer" || $column.javaType == "Long")0#else'0'#end#end +#macro(switchInactiveValue $column)#if($column.javaType == "Boolean")false#elseif($column.javaType == "Integer" || $column.javaType == "Long")1#else'1'#end#end +#if($enableStatus) +const ${statusField}ActiveValue = #if($statusColumn.javaType == "Boolean")true#elseif($statusColumn.javaType == "Integer" || $statusColumn.javaType == "Long")0#else'0'#end; +const ${statusField}InactiveValue = #if($statusColumn.javaType == "Boolean")false#elseif($statusColumn.javaType == "Integer" || $statusColumn.javaType == "Long")1#else'1'#end; + +#end +export default function ${BusinessName}Page() { + const actionRef = useRef(undefined); + const [form] = Form.useForm<${BusinessName}Form>(); + const userInfo = useUserStore(state => state.userInfo); +#if($needDict) + const dicts = useDict(${dicts}); +#end + const { ids, selectedOne, handleSelectionChange, clearSelection } = useTableSelection<${BusinessName}VO>( + row => row.${pkColumn.javaField} + ); + const [modalOpen, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false); + const [modalTitle, setModalTitle] = useState(''); +#if($enableExport) + const { updateExportParams, exportFile } = useTableExport(); +#end +#foreach($column in $columns) +#if($column.query && $column.htmlType == "datetime" && $column.queryType == "BETWEEN") +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) + const { applyDateRange: apply${AttrName}DateRange } = useDateRangeQuery('${AttrName}'); +#end +#end + + const canAdd = hasPermi(userInfo, ['${permissionPrefix}:add']); + const canEdit = hasPermi(userInfo, ['${permissionPrefix}:edit']); + const canRemove = hasPermi(userInfo, ['${permissionPrefix}:remove']); +#if($enableExport) + const canExport = hasPermi(userInfo, ['${permissionPrefix}:export']); +#end + + const openAdd = () => { + form.resetFields(); + form.setFieldsValue(default${BusinessName}Form); + setModalTitle('添加${functionName}'); + openModal(); + }; + + const openEdit = async (row?: ${BusinessName}VO) => { + const target = row || selectedOne; + if (!target?.${pkColumn.javaField}) return; + const res = await get${BusinessName}(target.${pkColumn.javaField}); + const data = #if($needDateField)toDayjsFields({ ...res.data }, [ +#foreach($column in $columns) +#if(($column.insert || $column.edit) && $column.htmlType == "datetime") + '${column.javaField}', +#end +#end + ])#else{ ...res.data }#end; +#foreach($column in $columns) +#if(($column.insert || $column.edit) && $column.htmlType == "checkbox") + if (typeof data.${column.javaField} === 'string') { + data.${column.javaField} = data.${column.javaField}.split(','); + } +#end +#end + form.resetFields(); + form.setFieldsValue(data); + setModalTitle('修改${functionName}'); + openModal(); + }; + + const submitForm = async (values: ${BusinessName}Form) => { + const submitValues = #if($needDateField)formatDateTimeFields({ ...values }, [ +#foreach($column in $columns) +#if(($column.insert || $column.edit) && $column.htmlType == "datetime") + '${column.javaField}', +#end +#end + ])#else{ ...values }#end; +#foreach($column in $columns) +#if(($column.insert || $column.edit) && $column.htmlType == "checkbox") + if (Array.isArray(submitValues.${column.javaField})) { + submitValues.${column.javaField} = submitValues.${column.javaField}.join(','); + } +#end +#end + submitValues.${pkColumn.javaField} ? await update${BusinessName}(submitValues) : await add${BusinessName}(submitValues); + message.success('操作成功'); + form.resetFields(); + actionRef.current?.reload(); + return true; + }; + + const remove = async (row?: ${BusinessName}VO) => { + await del${BusinessName}(row?.${pkColumn.javaField} || ids); + message.success('删除成功'); + clearSelection(); + actionRef.current?.reloadAndRest?.(); + }; + +#if($enableStatus) + const handleStatusChange = async (row: ${BusinessName}VO, checked: boolean) => { + const previousStatus = row.${statusField}; + const status = checked ? ${statusField}ActiveValue : ${statusField}InactiveValue; + const text = checked ? '启用' : '停用'; + try { + await confirmAction(`确认要"${text}"吗?`); + await change${BusinessName}Status(row.${pkColumn.javaField}, status); + message.success(`${text}成功`); + actionRef.current?.reload(); + } catch { + row.${statusField} = previousStatus; + actionRef.current?.reload(); + } + }; + +#end +#if($enableSort) + const handleSortChange = async (row: ${BusinessName}VO, value?: number | null) => { + await update${BusinessName}Sort(row.${pkColumn.javaField}, value || 0); + message.success('排序更新成功'); + actionRef.current?.reload(); + }; + +#end + const columns: ProColumns<${BusinessName}VO>[] = [ +#foreach($column in $columns) +#set($javaField=$column.javaField) +#set($dictType=$column.dictType) +#if($column.list) +#if($enableStatus && $statusField == $javaField) + { + title: '#comment($column)', + dataIndex: '${javaField}', + valueType: 'select', + width: 100, +#if("" != $dictType) + fieldProps: { options: dictOptions(dicts.${dictType}) }, +#end + render: (_, row) => ( + handleStatusChange(row, checked)} + /> + ) + }, +#elseif($enableSort && $sortField == $javaField) + { + title: '#comment($column)', + dataIndex: '${javaField}', + search: false, + width: 130, + render: (_, row) => handleSortChange(row, value)} /> + }, +#elseif($column.htmlType == "datetime") + { title: '#comment($column)', dataIndex: '${javaField}', valueType: 'dateTime', search: #if($column.query && $column.queryType != "BETWEEN")true#else false#end, width: 170 }, +#elseif($column.htmlType == "imageUpload") + { + title: '#comment($column)', + dataIndex: '${javaField}Url', + search: false, + width: 100, + render: (_, row) => + }, +#elseif($column.dictColumn) + { + title: '#comment($column)', + dataIndex: '${javaField}', + valueType: 'select', + fieldProps: { options: dictOptions(dicts.${dictType}) }, + render: (_, row) => + }, +#elseif($column.htmlType == "switch") + { + title: '#comment($column)', + dataIndex: '${javaField}', + valueType: 'select', + width: 100, + render: (_, row) => + }, +#else + { title: '#comment($column)', dataIndex: '${javaField}'#if(!$column.query), search: false#end }, +#end +#end +#if($column.query && $column.htmlType == "datetime" && $column.queryType == "BETWEEN") + { title: '#comment($column)', dataIndex: '${javaField}Range', valueType: 'dateTimeRange', hideInTable: true }, +#end +#end + { + title: '操作', + valueType: 'option', + width: 92, + fixed: 'right', + render: (_, row) => ( + , onClick: () => openEdit(row) }, + canRemove && { + key: 'delete', + label: '删除', + icon: , + danger: true, + confirm: `是否确认删除${functionName}编号为"#[[${row.]]#${pkColumn.javaField}#[[}]]#"的数据项?`, + onClick: () => remove(row) + } + ]} + /> + ) + } + ]; + + return ( + + #end> + actionRef={actionRef} + rowKey="${pkColumn.javaField}" + columns={columns} + scroll={{ x: 1000 }} + search={{ labelWidth: 90 }} + pagination={{ defaultPageSize: 10, showSizeChanger: true }} + rowSelection={{ selectedRowKeys: ids, onChange: handleSelectionChange }} + request={async params => { +#if($needDateRange) + let query = toPageQuery(params); +#foreach($column in $columns) +#if($column.query && $column.htmlType == "datetime" && $column.queryType == "BETWEEN") +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) + query = apply${AttrName}DateRange(query, params.${column.javaField}Range as [string, string] | undefined); + delete query.${column.javaField}Range; +#end +#end +#else + const query = toPageQuery(params); +#end +#if($enableExport) + updateExportParams(query); +#end + const res = await list${BusinessName}(query); + return toTableData(res); + }} + toolbar={{ title: '${functionName}列表' }} + toolBarRender={() => [ + canAdd && ( + + ), + canEdit && ( + + ), + canRemove && ( + remove()}> + + + )#if($enableExport), + canExport && ( + + )#end + ]} + /> + + + title={modalTitle} + open={modalOpen} + width={560} + form={form} + layout="vertical" + initialValues={default${BusinessName}Form} + modalProps={{ destroyOnHidden: true, onCancel: closeModal }} + onOpenChange={open => !open && closeModal()} + onFinish={submitForm} + > + + ); +} diff --git a/ruoyi-modules/ruoyi-gen/src/main/resources/vm/react/types.ts.vm b/ruoyi-modules/ruoyi-gen/src/main/resources/vm/react/types.ts.vm new file mode 100644 index 000000000..39b7df2a1 --- /dev/null +++ b/ruoyi-modules/ruoyi-gen/src/main/resources/vm/react/types.ts.vm @@ -0,0 +1,56 @@ +import type { BaseEntity#if(!$table.tree), PageQuery#end } from '@/api/types'; + +#macro(tsType $column)#if($column.javaField.indexOf("id") != -1 || $column.javaField.indexOf("Id") != -1)string | number#elseif($column.javaType == 'Long' || $column.javaType == 'Integer' || $column.javaType == 'Double' || $column.javaType == 'Float' || $column.javaType == 'BigDecimal')number#elseif($column.javaType == 'Boolean')boolean#else string#end#end + +export interface ${BusinessName}VO { +#foreach ($column in $columns) +#if($column.list || $column.pk) + /** + * $column.columnComment + */ + $column.javaField: #tsType($column); +#if($column.htmlType == "imageUpload") + /** + * ${column.columnComment}Url + */ + ${column.javaField}Url?: string; +#end +#end +#end +#if($table.tree) + /** + * 子对象 + */ + children?: ${BusinessName}VO[]; +#end +} + +export interface ${BusinessName}Form extends BaseEntity { +#foreach ($column in $columns) +#if($column.insert || $column.edit || $column.pk) + /** + * $column.columnComment + */ +#if($column.htmlType == "checkbox") + $column.javaField?: string | string[]; +#else + $column.javaField?: #tsType($column); +#end +#end +#end +} + +export interface ${BusinessName}Query#if(!$table.tree) extends PageQuery#end { +#foreach ($column in $columns) +#if($column.query) + /** + * $column.columnComment + */ + $column.javaField?: #tsType($column); +#end +#end + /** + * 日期范围参数 + */ + params?: Record; +} diff --git a/ruoyi-modules/ruoyi-gen/src/main/resources/vm/ts/api.ts.vm b/ruoyi-modules/ruoyi-gen/src/main/resources/vm/vue/api.ts.vm similarity index 100% rename from ruoyi-modules/ruoyi-gen/src/main/resources/vm/ts/api.ts.vm rename to ruoyi-modules/ruoyi-gen/src/main/resources/vm/vue/api.ts.vm diff --git a/ruoyi-modules/ruoyi-gen/src/main/resources/vm/ts/types.ts.vm b/ruoyi-modules/ruoyi-gen/src/main/resources/vm/vue/types.ts.vm similarity index 100% rename from ruoyi-modules/ruoyi-gen/src/main/resources/vm/ts/types.ts.vm rename to ruoyi-modules/ruoyi-gen/src/main/resources/vm/vue/types.ts.vm diff --git a/script/sql/oracle/oracle_ry_cloud.sql b/script/sql/oracle/oracle_ry_cloud.sql index e7cf04525..fe3da0e21 100644 --- a/script/sql/oracle/oracle_ry_cloud.sql +++ b/script/sql/oracle/oracle_ry_cloud.sql @@ -1083,6 +1083,7 @@ create table gen_table ( sub_table_fk_name varchar2(64) default null, class_name varchar2(100) default '', tpl_category varchar2(200) default 'crud', + frontend_type varchar2(30) default 'vue', package_name varchar2(100), module_name varchar2(30), business_name varchar2(30), @@ -1110,6 +1111,7 @@ comment on column gen_table.sub_table_name is '关联子表的表名'; comment on column gen_table.sub_table_fk_name is '子表关联的外键名'; comment on column gen_table.class_name is '实体类名称'; comment on column gen_table.tpl_category is '使用的模板(crud单表操作 tree树表操作)'; +comment on column gen_table.frontend_type is '前端模板类型'; comment on column gen_table.package_name is '生成包路径'; comment on column gen_table.module_name is '生成模块名'; comment on column gen_table.business_name is '生成业务名'; diff --git a/script/sql/postgres/postgres_ry_cloud.sql b/script/sql/postgres/postgres_ry_cloud.sql index 8a199b309..520a393af 100644 --- a/script/sql/postgres/postgres_ry_cloud.sql +++ b/script/sql/postgres/postgres_ry_cloud.sql @@ -1086,6 +1086,7 @@ create table if not exists gen_table sub_table_fk_name varchar(64) default ''::varchar, class_name varchar(100) default ''::varchar, tpl_category varchar(200) default 'crud'::varchar, + frontend_type varchar(30) default 'vue'::varchar, package_name varchar(100) default null::varchar, module_name varchar(30) default null::varchar, business_name varchar(30) default null::varchar, @@ -1112,6 +1113,7 @@ comment on column gen_table.sub_table_name is '关联子表的表名'; comment on column gen_table.sub_table_fk_name is '子表关联的外键名'; comment on column gen_table.class_name is '实体类名称'; comment on column gen_table.tpl_category is '使用的模板(CRUD单表操作 TREE树表操作)'; +comment on column gen_table.frontend_type is '前端模板类型'; comment on column gen_table.package_name is '生成包路径'; comment on column gen_table.module_name is '生成模块名'; comment on column gen_table.business_name is '生成业务名'; diff --git a/script/sql/ry-cloud.sql b/script/sql/ry-cloud.sql index aef0e42b7..cbdf5c6cb 100644 --- a/script/sql/ry-cloud.sql +++ b/script/sql/ry-cloud.sql @@ -816,6 +816,7 @@ create table gen_table ( sub_table_fk_name varchar(64) default null comment '子表关联的外键名', class_name varchar(100) default '' comment '实体类名称', tpl_category varchar(200) default 'crud' comment '使用的模板(crud单表操作 tree树表操作)', + frontend_type varchar(30) default 'vue' comment '前端模板类型', package_name varchar(100) comment '生成包路径', module_name varchar(30) comment '生成模块名', business_name varchar(30) comment '生成业务名',