# Conflicts:
#	yudao-module-ai/pom.xml
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertRecoverSceneRuleAction.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcher.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcher.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotSceneRuleConditionMatcher.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcher.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcher.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotSceneRuleTriggerMatcher.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcher.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml
#	yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcherTest.java
#	yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcherTest.java
#	yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java
#	yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java
#	yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcherTest.java
#	yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java
#	yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java
#	yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcherTest.java
#	yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
#	yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/IotDeviceServiceImpl.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
#	yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityRespVO.java
#	yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
#	yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
#	yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/WxPayClientConfig.java
#	yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java
This commit is contained in:
YunaiV
2025-10-12 15:34:09 +08:00
53 changed files with 2147 additions and 930 deletions

View File

@@ -33,7 +33,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2025.09-jdk8-SNAPSHOT</revision>
<revision>2025.10-jdk8-SNAPSHOT</revision>
<!-- Maven 相关 -->
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>

View File

@@ -6,12 +6,12 @@ Author: dhb52 (https://gitee.com/dhb52)
pip install simple-ddl-parser
or with uv
uv run --with simple-ddl-parser convertor.py postgres > ../postgresql/ruoyi-vue-pro.sql 239ms  四 5/22 21:03:16 2025
uv run --with simple-ddl-parser convertor.py sqlserver > ../sqlserver/ruoyi-vue-pro.sql
uv run --with simple-ddl-parser convertor.py kingbase > ../kingbase/ruoyi-vue-pro.sql
uv run --with simple-ddl-parser convertor.py opengauss > ../opengauss/ruoyi-vue-pro.sql
uv run --with simple-ddl-parser convertor.py oracle > ../oracle/ruoyi-vue-pro.sql
uv run --with simple-ddl-parser convertor.py dm8 > ../dm/ruoyi-vue-pro-dm8.sql
uv run --with simple-ddl-parser convertor.py postgres ../mysql/ruoyi-vue-pro.sql > ../postgresql/ruoyi-vue-pro.sql
uv run --with simple-ddl-parser convertor.py sqlserver ../mysql/ruoyi-vue-pro.sql > ../sqlserver/ruoyi-vue-pro.sql
uv run --with simple-ddl-parser convertor.py kingbase ../mysql/ruoyi-vue-pro.sql > ../kingbase/ruoyi-vue-pro.sql
uv run --with simple-ddl-parser convertor.py opengauss ../mysql/ruoyi-vue-pro.sql > ../opengauss/ruoyi-vue-pro.sql
uv run --with simple-ddl-parser convertor.py oracle ../mysql/ruoyi-vue-pro.sql > ../oracle/ruoyi-vue-pro.sql
uv run --with simple-ddl-parser convertor.py dm8 ../mysql/ruoyi-vue-pro.sql > ../dm/ruoyi-vue-pro-dm8.sql
"""
import argparse
@@ -24,6 +24,9 @@ from typing import Dict, Generator, Optional, Tuple, Union
from simple_ddl_parser import DDLParser
# 避免 Windows 系统使用默认的 GBK 编码
sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1)
PREAMBLE = """/*
Yudao Database Transfer Tool
@@ -919,9 +922,15 @@ def main():
help="目标数据库类型",
choices=["postgres", "oracle", "sqlserver", "dm8", "kingbase", "opengauss"],
)
parser.add_argument(
"path",
type=str,
help="源数据库脚本路径",
default="../mysql/ruoyi-vue-pro.sql"
)
args = parser.parse_args()
sql_file = pathlib.Path("../mysql/ruoyi-vue-pro.sql").resolve().as_posix()
sql_file = pathlib.Path(args.path).resolve().as_posix()
convertor = None
if args.type == "postgres":
convertor = PostgreSQLConvertor(sql_file)

View File

@@ -14,7 +14,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2025.09-jdk8-SNAPSHOT</revision>
<revision>2025.10-jdk8-SNAPSHOT</revision>
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 -->
<spring.framework.version>5.3.39</spring.framework.version>
@@ -27,7 +27,7 @@
<!-- DB 相关 -->
<druid.version>1.2.27</druid.version>
<mybatis.version>3.5.19</mybatis.version>
<mybatis-plus.version>3.5.12</mybatis-plus.version>
<mybatis-plus.version>3.5.14</mybatis-plus.version>
<mybatis-plus-join.version>1.5.4</mybatis-plus-join.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
<easy-trans.version>3.0.6</easy-trans.version>
@@ -63,7 +63,7 @@
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
<commons-net.version>3.11.1</commons-net.version>
<commons-lang3.version>3.18.0</commons-lang3.version>
<jsch.version>0.1.55</jsch.version>
<jsch.version>2.27.3</jsch.version>
<tika-core.version>2.9.3</tika-core.version> <!-- JDK8 不能从 2.9.3 升级到 3.X会报 JDK8 不支持 -->
<ip2region.version>2.7.0</ip2region.version>
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
@@ -535,7 +535,7 @@
<version>${commons-net.version}</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<groupId>com.github.mwiede</groupId>
<artifactId>jsch</artifactId> <!-- 解决 sftp 连接 -->
<version>${jsch.version}</version>
</dependency>

View File

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.baomidou.mybatisplus.core.handlers.IJsonTypeHandler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
@@ -80,8 +81,8 @@ public class YudaoMybatisAutoConfiguration {
throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType));
}
@Bean
public JacksonTypeHandler jacksonTypeHandler(List<ObjectMapper> objectMappers) {
@Bean // 特殊:返回结果使用 Object 而不用 JacksonTypeHandler 的原因,避免因为 JacksonTypeHandler 被 mybatis 全局使用!
public Object jacksonTypeHandler(List<ObjectMapper> objectMappers) {
// 特殊:设置 JacksonTypeHandler 的 ObjectMapper
ObjectMapper objectMapper = CollUtil.getFirst(objectMappers);
if (objectMapper == null) {

View File

@@ -60,6 +60,12 @@ public enum DbTypeEnum {
* 人大金仓
*/
KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"),
/**
* OceanBase
*/
OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET('#{value}', #{column}) <> 0")
;
public static final Map<String, DbTypeEnum> MAP_BY_NAME = Arrays.stream(values())

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.swagger.config;
import com.github.xiaoymin.knife4j.spring.configuration.Knife4jAutoConfiguration;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
@@ -12,6 +13,7 @@ import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.*;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -39,7 +41,7 @@ import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_
*
* @author 芋道源码
*/
@AutoConfiguration
@AutoConfiguration(before = Knife4jAutoConfiguration.class) // before 原因,保证覆写的 Knife4jOpenApiCustomizer 先生效!相关 https://github.com/YunaiV/ruoyi-vue-pro/issues/954 讨论
@ConditionalOnClass({OpenAPI.class})
@EnableConfigurationProperties(SwaggerProperties.class)
@ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
@@ -123,6 +125,7 @@ public class YudaoSwaggerAutoConfiguration {
.addOperationCustomizer((operation, handlerMethod) -> operation
.addParametersItem(buildTenantHeaderParameter())
.addParametersItem(buildSecurityHeaderParameter()))
.addOperationCustomizer(buildOperationIdCustomizer())
.build();
}
@@ -154,5 +157,26 @@ public class YudaoSwaggerAutoConfiguration {
.schema(new StringSchema()._default("Bearer test1").name(HEADER_TENANT_ID).description("认证 Token")); // 默认:使用用户编号为 1
}
/**
* 核心自定义OperationId生成规则组合「类名前缀 + 方法名」
*
* @see <a href="https://github.com/YunaiV/ruoyi-vue-pro/issues/957">app-api 前缀不生效,都是使用 admin-api</a>
*/
private static OperationCustomizer buildOperationIdCustomizer() {
return (operation, handlerMethod) -> {
// 1. 获取控制器类名(如 UserController
String className = handlerMethod.getBeanType().getSimpleName();
// 2. 提取类名前缀(去除 Controller 后缀,如 UserController -> User
String classPrefix = className.replaceAll("Controller$", "");
// 3. 获取方法名(如 list
String methodName = handlerMethod.getMethod().getName();
// 4. 组合生成 operationId如 User_list
String operationId = classPrefix + "_" + methodName;
// 5. 设置自定义 operationId
operation.setOperationId(operationId);
return operation;
};
}
}

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.web.config;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter;
@@ -7,11 +8,13 @@ import cn.iocoder.yudao.framework.web.core.filter.DemoFilter;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import com.google.common.collect.Maps;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
@@ -22,40 +25,58 @@ import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.Map;
import java.util.function.Predicate;
@AutoConfiguration
@EnableConfigurationProperties(WebProperties.class)
public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
public class YudaoWebAutoConfiguration {
@Resource
private WebProperties webProperties;
/**
* 应用名
*/
@Value("${spring.application.name}")
private String applicationName;
@Bean
public WebMvcRegistrations webMvcRegistrations(WebProperties webProperties) {
return new WebMvcRegistrations() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurePathMatch(configurer, webProperties.getAdminApi());
configurePathMatch(configurer, webProperties.getAppApi());
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
// 实例化时就带上前缀
mapping.setPathPrefixes(buildPathPrefixes(webProperties));
return mapping;
}
/**
* 构建 prefix → 匹配条件的映射
*/
private Map<String, Predicate<Class<?>>> buildPathPrefixes(WebProperties webProperties) {
AntPathMatcher antPathMatcher = new AntPathMatcher(".");
Map<String, Predicate<Class<?>>> pathPrefixes = Maps.newLinkedHashMapWithExpectedSize(2);
putPathPrefix(pathPrefixes, webProperties.getAdminApi(), antPathMatcher);
putPathPrefix(pathPrefixes, webProperties.getAppApi(), antPathMatcher);
return pathPrefixes;
}
/**
* 设置 API 前缀,仅仅匹配 controller 包下的
*
* @param configurer 配置
* @param api API 配置
*/
private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) {
AntPathMatcher antPathMatcher = new AntPathMatcher(".");
configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class)
&& antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包
private void putPathPrefix(Map<String, Predicate<Class<?>>> pathPrefixes, WebProperties.Api api, AntPathMatcher matcher) {
if (api == null || StrUtil.isEmpty(api.getPrefix())) {
return;
}
pathPrefixes.put(api.getPrefix(), // api 前缀
clazz -> clazz.isAnnotationPresent(RestController.class)
&& matcher.match(api.getController(), clazz.getPackage().getName()));
}
};
}
@Bean

View File

@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 流程实例的打印数据 Response VO")
@Data
public class BpmProcessPrintDataRespVO {
@Schema(description = "流程实例数据")
private BpmProcessInstanceRespVO processInstance;
@Schema(description = "是否开启自定义打印模板", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean printTemplateEnable;
@Schema(description = "自定义打印模板 HTML")
private String printTemplateHtml;
@Schema(description = "审批任务列表")
private List<Task> tasks;
@Schema(description = "流程任务")
@Data
public static class Task {
@Schema(description = "流程任务的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private String id;
@Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
private String name;
@Schema(description = "签名 URL", example = "https://www.iocoder.cn/sign.png")
private String signPicUrl;
@Schema(description = "任务描述", requiredMode = Schema.RequiredMode.REQUIRED)
private String description; // 该字段由后端拼接
}
}

View File

@@ -94,7 +94,7 @@
<artifactId>commons-net</artifactId> <!-- 文件客户端:解决 ftp 连接 -->
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<groupId>com.github.mwiede</groupId>
<artifactId>jsch</artifactId> <!-- 文件客户端:解决 sftp 连接 -->
</dependency>
<!-- 文件客户端解决阿里云、腾讯云、minio 等 S3 连接 -->

View File

@@ -26,6 +26,7 @@ import javax.annotation.security.PermitAll;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -99,8 +100,10 @@ public class FileController {
if (StrUtil.isEmpty(path)) {
throw new IllegalArgumentException("结尾的 path 路径必须传递");
}
// 解码,解决中文路径的问题 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/807/
path = URLUtil.decode(path);
// 解码,解决中文路径的问题
// https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/807/
// https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1432/
path = URLUtil.decode(path, StandardCharsets.UTF_8, false);
// 读取内容
byte[] content = fileService.getFileContent(configId, path);

View File

@@ -21,9 +21,8 @@ public enum CodegenFrontTypeEnum {
VUE3_VBEN5_ANTD_SCHEMA(40), // Vue3 VBEN5 + ANTD + schema 模版
VUE3_VBEN5_ANTD_GENERAL(41), // Vue3 VBEN5 + ANTD 标准模版
// TODO @puhui999:50、51 会好点;
VUE3_VBEN5_EP_SCHEMA(42), // Vue3 VBEN5 + EP + schema 模版
VUE3_VBEN5_EP_GENERAL(43), // Vue3 VBEN5 + EP 标准模版
VUE3_VBEN5_EP_SCHEMA(50), // Vue3 VBEN5 + EP + schema 模版
VUE3_VBEN5_EP_GENERAL(51), // Vue3 VBEN5 + EP 标准模版
;
/**

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.ftp.Ftp;
import cn.hutool.extra.ftp.FtpConfig;
import cn.hutool.extra.ftp.FtpException;
import cn.hutool.extra.ftp.FtpMode;
import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient;
@@ -18,6 +19,15 @@ import java.io.ByteArrayOutputStream;
*/
public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
/**
* 连接超时时间,单位:毫秒
*/
private static final Long CONNECTION_TIMEOUT = 3000L;
/**
* 读写超时时间,单位:毫秒
*/
private static final Long SO_TIMEOUT = 10000L;
private Ftp ftp;
public FtpFileClient(Long id, FtpFileClientConfig config) {
@@ -26,9 +36,12 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
@Override
protected void doInit() {
// 初始化 Ftp 对象
this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(),
CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode()));
// 初始化 Ftp 对象https://gitee.com/zhijiantianya/yudao-cloud/pulls/207/
FtpConfig ftpConfig = new FtpConfig(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(),
CharsetUtil.CHARSET_UTF_8, null, null);
ftpConfig.setConnectionTimeout(CONNECTION_TIMEOUT);
ftpConfig.setSoTimeout(SO_TIMEOUT);
this.ftp = new Ftp(ftpConfig, FtpMode.valueOf(config.getMode()));
}
@Override

View File

@@ -1,9 +1,14 @@
package cn.iocoder.yudao.module.infra.framework.file.core.client.sftp;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.ftp.FtpConfig;
import cn.hutool.extra.ssh.JschRuntimeException;
import cn.hutool.extra.ssh.Sftp;
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient;
import com.jcraft.jsch.JSch;
import java.io.File;
@@ -14,6 +19,20 @@ import java.io.File;
*/
public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
/**
* 连接超时时间,单位:毫秒
*/
private static final Long CONNECTION_TIMEOUT = 3000L;
/**
* 读写超时时间,单位:毫秒
*/
private static final Long SO_TIMEOUT = 10000L;
static {
// 某些旧的 sftp 服务器仅支持 ssh-dss 协议,该协议并不安全,默认不支持该协议,按需添加
JSch.setConfig("server_host_key", JSch.getConfig("server_host_key") + ",ssh-dss");
}
private Sftp sftp;
public SftpFileClient(Long id, SftpFileClientConfig config) {
@@ -22,18 +41,27 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
@Override
protected void doInit() {
// 初始化 Ftp 对象
this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword());
// 初始化 Sftp 对象
FtpConfig ftpConfig = new FtpConfig(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(),
CharsetUtil.CHARSET_UTF_8, null, null);
ftpConfig.setConnectionTimeout(CONNECTION_TIMEOUT);
ftpConfig.setSoTimeout(SO_TIMEOUT);
this.sftp = new Sftp(ftpConfig);
}
@Override
public String upload(byte[] content, String path, String type) {
// 执行写入
String filePath = getFilePath(path);
String fileName = FileUtil.getName(filePath);
String dir = StrUtil.removeSuffix(filePath, fileName);
File file = FileUtils.createTempFile(content);
reconnectIfTimeout();
sftp.mkDirs(FileUtil.getParent(filePath, 1)); // 需要创建父目录,不然会报错
sftp.upload(filePath, file);
sftp.mkDirs(dir); // 需要创建父目录,不然会报错
boolean success = sftp.upload(filePath, file);
if (!success) {
throw new JschRuntimeException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath));
}
// 拼接返回路径
return super.formatFileUrl(config.getDomain(), path);
}

View File

@@ -343,7 +343,7 @@ public class CodegenEngine {
filePath = formatFilePath(filePath, bindingMap);
String content = templateEngine.getTemplate(vmPath).render(bindingMap);
// 格式化代码
content = prettyCode(content);
content = prettyCode(content, vmPath);
result.put(filePath, content);
}
@@ -383,11 +383,14 @@ public class CodegenEngine {
* 如果不处理Vue 的 Pretty 格式校验可能会报错
*
* @param content 格式化前的代码
* @param vmPath 模板路径
* @return 格式化后的代码
*/
private String prettyCode(String content) {
// Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错
private String prettyCode(String content, String vmPath) {
// Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(需要排除 vben5
if (!StrUtil.contains(vmPath, "vben5")) {
content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }");
}
// Vue 界面:去除多的 dateFormatter只有一个的情况下说明没使用到
if (StrUtil.count(content, "dateFormatter") == 1) {
content = StrUtils.removeLineContains(content, "dateFormatter");

View File

@@ -217,7 +217,7 @@ const handleDeleteBatch = async () => {
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: ${subSimpleClassName}[]) => {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
#end

View File

@@ -374,7 +374,7 @@ const handleDeleteBatch = async () => {
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: ${simpleClassName}[]) => {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end

View File

@@ -98,7 +98,7 @@ export function delete${simpleClassName}List(ids: number[]) {
/** 导出${table.classComment} */
export function export${simpleClassName}(params: any) {
return requestClient.download('${baseURL}/export-excel', params);
return requestClient.download('${baseURL}/export-excel', { params });
}
## 特殊:主子表专属逻辑

View File

@@ -1,12 +1,15 @@
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { Rule } from 'ant-design-vue/es/form';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
import { DICT_TYPE, getDictOptions } from '#/utils';
#if($table.templateType == 2)## 树表需要导入这些
import { get${simpleClassName}List } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { handleTree } from '@vben/utils'
@@ -20,7 +23,6 @@ import { handleTree } from '@vben/utils'
#end
#end
import { computed, ref } from 'vue';
import { $t } from '#/locales';
import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
@@ -70,7 +72,7 @@ const getTitle = computed(() => {
#end
/** 重置表单 */
const resetForm = () => {
function resetForm() {
formData.value = {
#foreach ($column in $columns)
#if ($column.createOperation || $column.updateOperation)
@@ -88,7 +90,7 @@ const resetForm = () => {
## 特殊:树表专属逻辑
#if ( $table.templateType == 2 )
/** 获得${table.classComment}树 */
const get${simpleClassName}Tree = async () => {
async function get${simpleClassName}Tree() {
${classNameVar}Tree.value = []
const data = await get${simpleClassName}List({});
data.unshift({
@@ -147,7 +149,6 @@ const [Modal, modalApi] = useVbenModal({
emit('success');
message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_process_msg',
});
} finally {
modalApi.unlock();

View File

@@ -1,18 +1,18 @@
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { ref, h, reactive, onMounted, nextTick } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { cloneDeep, formatDateTime } from '@vben/utils';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
import { cloneDeep, downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
import { Button, message,Tabs,Pagination,Form,RangePicker,DatePicker,Select,Input } from 'ant-design-vue';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
import ${simpleClassName}Form from './modules/form.vue';
import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
import { ContentWrap } from '#/components/content-wrap';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { TableToolbar } from '#/components/table-toolbar';
import { useTableToolbar } from '#/hooks';
## 特殊:主子表专属逻辑
#if ( $table.templateType == 11 || $table.templateType == 12 )
@@ -23,7 +23,6 @@ import { useTableToolbar } from '#/hooks';
#end
#end
import { ref, h, reactive,onMounted,nextTick } from 'vue';
import { $t } from '#/locales';
#if (${table.templateType} == 2)## 树表接口
import { handleTree,isEmpty } from '@vben/utils'
@@ -77,7 +76,7 @@ const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
async function getList() {
loading.value = true
try {
const params = cloneDeep(queryParams) as any;
@@ -104,7 +103,7 @@ const getList = async () => {
}
/** 搜索按钮操作 */
const handleQuery = () => {
function handleQuery() {
#if ( $table.templateType != 2 )
queryParams.pageNo = 1
#end
@@ -112,7 +111,7 @@ const handleQuery = () => {
}
/** 重置按钮操作 */
const resetQuery = () => {
function resetQuery() {
queryFormRef.value.resetFields()
handleQuery()
}
@@ -124,7 +123,7 @@ const [FormModal, formModalApi] = useVbenModal({
/** 创建${table.classComment} */
function handleCreate() {
formModalApi.setData({}).open();
formModalApi.setData(null).open();
}
/** 编辑${table.classComment} */
@@ -144,14 +143,10 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0,
key: 'action_process_msg',
});
try {
await delete${simpleClassName}(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_process_msg',
});
await delete${simpleClassName}(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
await getList();
} finally {
hideLoading();
@@ -164,12 +159,11 @@ async function handleDeleteBatch() {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
duration: 0,
key: 'action_process_msg',
});
try {
await delete${simpleClassName}List(checkedIds.value);
checkedIds.value = [];
message.success( $t('ui.actionMessage.deleteSuccess') );
message.success($t('ui.actionMessage.deleteSuccess'));
await getList();
} finally {
hideLoading();
@@ -182,12 +176,12 @@ function handleRowCheckboxChange({
}: {
records: ${simpleClassName}Api.${simpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
/** 导出表格 */
async function onExport() {
async function handleExport() {
try {
exportLoading.value = true;
const data = await export${simpleClassName}(queryParams);
@@ -200,7 +194,7 @@ try {
#if (${table.templateType} == 2)
/** 切换树形展开/收缩状态 */
const isExpanded = ref(true);
function toggleExpand() {
function handleExpand() {
isExpanded.value = !isExpanded.value;
tableRef.value?.setAllTreeExpand(isExpanded.value);
}
@@ -303,12 +297,12 @@ onMounted(() => {
<!-- 列表 -->
<ContentWrap title="${table.classComment}">
<template #extra>
<TableToolbar
<VbenVxeTableToolbar
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>
#if (${table.templateType} == 2)
<Button @click="toggleExpand" class="mr-2">
<Button @click="handleExpand" class="mr-2">
{{ isExpanded ? '收缩' : '展开' }}
</Button>
#end
@@ -326,7 +320,7 @@ onMounted(() => {
type="primary"
class="ml-2"
:loading="exportLoading"
@click="onExport"
@click="handleExport"
v-access:code="['${permissionPrefix}:export']"
>
{{ $t('ui.actionTitle.export') }}

View File

@@ -3,29 +3,32 @@
#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { Rule } from 'ant-design-vue/es/form';
import type { Rule } from 'ant-design-vue/es/form';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { useVbenModal } from '@vben/common-ui';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { computed, ref } from 'vue';
import { computed, ref } from 'vue';
import { $t } from '#/locales';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { cloneDeep, formatDateTime } from '@vben/utils';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { $t } from '#/locales';
const emit = defineEmits(['success']);
const getTitle = computed(() => {
import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
const emit = defineEmits(['success']);
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['${subTable.classComment}'])
: $t('ui.actionTitle.create', ['${subTable.classComment}']);
});
});
const formRef = ref();
const formData = ref<Partial<${simpleClassName}Api.${subSimpleClassName}>>({
const formRef = ref();
const formData = ref<Partial<${simpleClassName}Api.${subSimpleClassName}>>({
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#if ($column.htmlType == "checkbox")
@@ -35,17 +38,17 @@
#end
#end
#end
});
const rules: Record<string, Rule[]> = {
});
const rules: Record<string, Rule[]> = {
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
#set($comment=$column.columnComment)
$column.javaField: [{ required: true, message: '${comment}不能为空', trigger: #if($column.htmlType == 'select')'change'#else'blur'#end }],
#end
#end
};
};
const [Modal, modalApi] = useVbenModal({
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
await formRef.value?.validate();
@@ -59,7 +62,6 @@
emit('success');
message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_process_msg',
});
} finally {
modalApi.unlock();
@@ -87,10 +89,10 @@
// 设置到 values
formData.value = data;
},
});
});
/** 重置表单 */
const resetForm = () => {
/** 重置表单 */
function resetForm(){
formData.value = {
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
@@ -103,7 +105,7 @@
#end
};
formRef.value?.resetFields();
}
}
</script>
<template>

View File

@@ -5,12 +5,15 @@
#set ($subClassNameVar = $subClassNameVars.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { message, Tabs, Form, Input, Textarea,Button, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker } from 'ant-design-vue';
import { computed, ref, h, onMounted,watch,nextTick } from 'vue';
import { $t } from '#/locales';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { computed, ref, h, onMounted,watch,nextTick } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { message, Tabs, Form, Input, Textarea,Button, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker } from 'ant-design-vue';
import { $t } from '#/locales';
#if ($subTable.subJoinMany) ## 一对多
import type { VxeTableInstance } from '#/adapter/vxe-table';
@@ -30,13 +33,14 @@ const props = defineProps<{
#if ($subTable.subJoinMany) ## 一对多
const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据
const tableRef = ref<VxeTableInstance>();
/** 添加${subTable.classComment} */
const onAdd = async () => {
async function handleAdd() {
await tableRef.value?.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1);
}
/** 删除${subTable.classComment} */
const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => {
async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
await tableRef.value?.remove(row);
}
@@ -46,9 +50,11 @@ defineExpose({
const data = list.value as ${simpleClassName}Api.${subSimpleClassName}[];
const removeRecords = tableRef.value?.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
const insertRecords = tableRef.value?.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
?.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
return [
...data.filter(
(row) => !removeRecords.some((removed) => removed.id === row.id),
),
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
},
});
@@ -123,25 +129,25 @@ watch(
#if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
#elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<Input v-model:value="row.${javaField}" />
</template>
</vxe-column>
#elseif($column.htmlType == "imageUpload")## 图片上传
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<ImageUpload v-model:value="row.${javaField}" />
</template>
</vxe-column>
#elseif($column.htmlType == "fileUpload")## 文件上传
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<FileUpload v-model:value="row.${javaField}" />
</template>
</vxe-column>
#elseif($column.htmlType == "select")## 下拉框
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<Select v-model:value="row.${javaField}" placeholder="请选择${comment}">
#if ("" != $dictType)## 有数据字典
<Select.Option
@@ -159,7 +165,7 @@ watch(
</vxe-column>
#elseif($column.htmlType == "checkbox")## 多选框
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<CheckboxGroup v-model:value="row.${javaField}">
#if ("" != $dictType)## 有数据字典
<Checkbox
@@ -177,7 +183,7 @@ watch(
</vxe-column>
#elseif($column.htmlType == "radio")## 单选框
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<RadioGroup v-model:value="row.${javaField}">
#if ("" != $dictType)## 有数据字典
<Radio
@@ -195,7 +201,7 @@ watch(
</vxe-column>
#elseif($column.htmlType == "datetime")## 时间框
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<DatePicker
v-model:value="row.${javaField}"
:showTime="true"
@@ -206,7 +212,7 @@ watch(
</vxe-column>
#elseif($column.htmlType == "textarea" || $column.htmlType == "editor")## 文本框
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<Textarea v-model:value="row.${javaField}" />
</template>
</vxe-column>
@@ -214,12 +220,12 @@ watch(
#end
#end
<vxe-column field="operation" title="操作" align="center">
<template #default="{row}">
<template #default="{ row }">
<Button
size="small"
type="link"
danger
@click="onDelete(row as any)"
@click="handleDelect(row)"
v-access:code="['${permissionPrefix}:delete']"
>
{{ $t('ui.actionTitle.delete') }}
@@ -228,7 +234,7 @@ watch(
</vxe-column>
</vxe-table>
<div class="flex justify-center mt-4">
<Button :icon="h(Plus)" type="primary" ghost @click="onAdd" v-access:code="['${permissionPrefix}:create']">
<Button :icon="h(Plus)" type="primary" ghost @click="handleAdd" v-access:code="['${permissionPrefix}:create']">
{{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }}
</Button>
</div>

View File

@@ -6,37 +6,39 @@
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';
import { cloneDeep, formatDateTime } from '@vben/utils';
import { ContentWrap } from '#/components/content-wrap';
import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { DictTag } from '#/components/dict-tag';
import { getRangePickerDefaultProps } from '#/utils';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { ContentWrap } from '#/components/content-wrap';
#if ($table.templateType == 11) ## erp
import { useVbenModal } from '@vben/common-ui';
import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { message,Button, Tabs,Pagination, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox,RangePicker, DatePicker, TreeSelect } from 'ant-design-vue';
import { Plus, Trash2 } from '@vben/icons';
import { $t } from '#/locales';
import { TableToolbar } from '#/components/table-toolbar';
import { useTableToolbar } from '#/hooks';
import { useVbenModal } from '@vben/common-ui';
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { message,Button, Tabs,Pagination, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox,RangePicker, DatePicker, TreeSelect } from 'ant-design-vue';
import { Plus, Trash2 } from '@vben/icons';
import { $t } from '#/locales';
#end
#if ($table.templateType == 11) ## erp
import { delete${subSimpleClassName},#if ($deleteBatchEnable) delete${subSimpleClassName}List,#end get${subSimpleClassName}Page } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { isEmpty } from '@vben/utils';
#else
#if ($subTable.subJoinMany) ## 一对多
import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
#else
import { get${subSimpleClassName}By${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
#end
import { delete${subSimpleClassName},#if ($deleteBatchEnable) delete${subSimpleClassName}List,#end get${subSimpleClassName}Page } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { isEmpty } from '@vben/utils';
#else
#if ($subTable.subJoinMany) ## 一对多
import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
#else
import { get${subSimpleClassName}By${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
#end
#end
const props = defineProps<{
@@ -44,10 +46,10 @@ const props = defineProps<{
}>()
#if ($table.templateType == 11) ## erp
const [FormModal, formModalApi] = useVbenModal({
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: ${subSimpleClassName}Form,
destroyOnClose: true,
});
});
/** 创建${subTable.classComment} */
function handleCreate() {
@@ -68,14 +70,10 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0,
key: 'action_process_msg',
});
try {
await delete${subSimpleClassName}(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_process_msg',
});
await delete${subSimpleClassName}(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
await getList();
} finally {
hideLoading();
@@ -88,12 +86,11 @@ async function handleDeleteBatch() {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
duration: 0,
key: 'action_process_msg',
});
try {
await delete${subSimpleClassName}List(checkedIds.value);
checkedIds.value = [];
message.success( $t('ui.actionMessage.deleteSuccess') );
message.success($t('ui.actionMessage.deleteSuccess'));
await getList();
} finally {
hideLoading();
@@ -106,19 +103,19 @@ function handleRowCheckboxChange({
}: {
records: ${simpleClassName}Api.${subSimpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
#end
const loading = ref(true) // 列表的加载中
const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据
const loading = ref(true) // 列表的加载中
const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据
#if ($table.templateType == 11) ## erp
const total = ref(0) // 列表的总页数
const total = ref(0) // 列表的总页数
#end
#if ($table.templateType == 11) ## erp
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
#foreach ($column in $subColumns)
@@ -131,22 +128,22 @@ function handleRowCheckboxChange({
#end
#end
#end
})
})
/** 搜索按钮操作 */
const handleQuery = () => {
function handleQuery() {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
function resetQuery() {
queryFormRef.value.resetFields()
handleQuery()
}
#end
/** 查询列表 */
const getList = async () => {
async function getList() {
loading.value = true
try {
if (!props.${subJoinColumn.javaField}){
@@ -178,10 +175,10 @@ const resetQuery = () => {
} finally {
loading.value = false
}
}
}
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
if (!val) {
@@ -191,7 +188,7 @@ const resetQuery = () => {
await getList()
},
{ immediate: true },
);
);
#if ($table.templateType == 11) ## erp
/** 初始化 */
@@ -292,7 +289,7 @@ onMounted(() => {
<!-- 列表 -->
<ContentWrap title="${table.classComment}">
<template #extra>
<TableToolbar
<VbenVxeTableToolbar
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>

View File

@@ -1,167 +1 @@
import type { PageParam, PageResult } from '@vben/request';
import type { Dayjs } from 'dayjs';
import { requestClient } from '#/api/request';
#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
export namespace ${simpleClassName}Api {
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subColumns = $subColumnsList.get($index))##当前字段数组
/** ${subTable.classComment}信息 */
export interface ${subSimpleClassName} {
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}
#else
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
#end
#end
#end
}
#end
/** ${table.classComment}信息 */
export interface ${simpleClassName} {
#foreach ($column in $columns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}
#else
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
#end
#end
#end
#if ( $table.templateType == 2 )
children?: ${simpleClassName}[];
#end
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#if ( $subTable.subJoinMany )
${subSimpleClassName.toLowerCase()}s?: ${subSimpleClassName}[]
#else
${subSimpleClassName.toLowerCase()}?: ${subSimpleClassName}
#end
#end
#end
}
}
#if ( $table.templateType != 2 )
/** 查询${table.classComment}分页 */
export function get${simpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${simpleClassName}>>('${baseURL}/page', { params });
}
#else
/** 查询${table.classComment}列表 */
export function get${simpleClassName}List(params: any) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}[]>('${baseURL}/list', { params });
}
#end
/** 查询${table.classComment}详情 */
export function get${simpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}>(`${baseURL}/get?id=${id}`);
}
/** 新增${table.classComment} */
export function create${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
return requestClient.post('${baseURL}/create', data);
}
/** 修改${table.classComment} */
export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
return requestClient.put('${baseURL}/update', data);
}
/** 删除${table.classComment} */
export function delete${simpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/delete?id=${id}`);
}
#if ( $table.templateType != 2 && $deleteBatchEnable)
/** 批量删除${table.classComment} */
export function delete${simpleClassName}List(ids: number[]) {
return requestClient.delete(`${baseURL}/delete-list?ids=${ids.join(',')}`)
}
#end
/** 导出${table.classComment} */
export function export${simpleClassName}(params: any) {
return requestClient.download('${baseURL}/export-excel', params);
}
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段
#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
// ==================== 子表($subTable.classComment ====================
## 情况一MASTER_ERP 时,需要分查询页子表
#if ( $table.templateType == 11 )
/** 获得${subTable.classComment}分页 */
export function get${subSimpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${subSimpleClassName}>>(`${baseURL}/${subSimpleClassName_strikeCase}/page`, { params });
}
## 情况二:非 MASTER_ERP 时,需要列表查询子表
#else
#if ( $subTable.subJoinMany )
/** 获得${subTable.classComment}列表 */
export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}[]>(`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#else
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#end
#end
## 特殊MASTER_ERP 时,支持单个的新增、修改、删除操作
#if ( $table.templateType == 11 )
/** 新增${subTable.classComment} */
export function create${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data);
}
/** 修改${subTable.classComment} */
export function update${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data);
}
/** 删除${subTable.classComment} */
export function delete${subSimpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete?id=${id}`);
}
#if ($deleteBatchEnable)
/** 批量删除${subTable.classComment} */
export function delete${subSimpleClassName}List(ids: number[]) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`)
}
#end
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`);
}
#end
#end
#parse("codegen/vue3_vben5_ele/schema/api/api.ts.vm")

View File

@@ -1,18 +1,17 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { z } from '#/adapter/form';
import {
DICT_TYPE,
getDictOptions,
getRangePickerDefaultProps,
} from '#/utils';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
#if(${table.templateType} == 2)## 树表需要导入这些
import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}';
import { handleTree } from '@vben/utils';
#end
import { getRangePickerDefaultProps } from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@@ -125,7 +124,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber',
componentProps: {
min: 0,
controlsPosition: 'right',
placeholder: '请输入${comment}',
},
#end
@@ -186,7 +184,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>['columns'] {
export function useGridColumns(): VxeTableGridOptions<${apiName}.${simpleClassName}>['columns'] {
return [
#if ($table.templateType != 2 && $deleteBatchEnable)
{ type: 'checkbox', width: 40 },
@@ -327,7 +325,6 @@ export function use${subSimpleClassName}FormSchema(): VbenFormSchema[] {
component: 'InputNumber',
componentProps: {
min: 0,
controlsPosition: 'right',
placeholder: '请输入${comment}',
},
#end
@@ -389,7 +386,7 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] {
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
return [
#if ($table.templateType != 2 && $deleteBatchEnable)
{ type: 'checkbox', width: 40 },
@@ -426,7 +423,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${sim
#else
#if ($subTable.subJoinMany) ## 一对多
/** 新增/修改列表的字段 */
export function use${subSimpleClassName}GridEditColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] {
export function use${subSimpleClassName}GridEditColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
return [
#foreach($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
@@ -561,7 +558,6 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${sim
component: 'InputNumber',
componentProps: {
min: 0,
controlsPosition: 'right',
placeholder: '请输入${comment}',
},
#end

View File

@@ -1,8 +1,16 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, Tabs, Checkbox, Input, Textarea, Select,RadioGroup,CheckboxGroup, DatePicker } from 'ant-design-vue';
import { message#if ($table.templateType == 11), Tabs#end } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
import { $t } from '#/locales';
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#foreach ($subSimpleClassName in $subSimpleClassNames)
@@ -12,21 +20,15 @@ import { message, Tabs, Checkbox, Input, Textarea, Select,RadioGroup,CheckboxGro
#end
#end
import { computed, ref } from 'vue';
import { $t } from '#/locales';
import { useVbenForm } from '#/adapter/form';
import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<${simpleClassName}Api.${simpleClassName}>();
const formData = ref<${apiName}.${simpleClassName}>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['${table.classComment}'])
: $t('ui.actionTitle.create', ['${table.classComment}']);
});
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#if ( $subTables && $subTables.size() > 0 )
@@ -51,7 +53,7 @@ const [Form, formApi] = useVbenForm({
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
@@ -60,8 +62,8 @@ const [Modal, modalApi] = useVbenModal({
if (!valid) {
return;
}
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#if ( $subTables && $subTables.size() > 0 )
// 校验子表单
#foreach ($subTable in $subTables)
@@ -78,12 +80,12 @@ const [Modal, modalApi] = useVbenModal({
#end
#end
#end
#end
#end
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ${simpleClassName}Api.${simpleClassName};
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
const data = (await formApi.getValues()) as ${apiName}.${simpleClassName};
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#if ( $subTables && $subTables.size() > 0 )
// 拼接子表的数据
#foreach ($subTable in $subTables)
@@ -96,13 +98,13 @@ const [Modal, modalApi] = useVbenModal({
#end
#end
#end
#end
#end
try {
await (formData.value?.id ? update${simpleClassName}(data) : create${simpleClassName}(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success( $t('ui.actionMessage.operationSuccess') );
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
@@ -113,21 +115,22 @@ const [Modal, modalApi] = useVbenModal({
return;
}
// 加载数据
let data = modalApi.getData<${simpleClassName}Api.${simpleClassName}>();
if (!data) {
const data = modalApi.getData<${apiName}.${simpleClassName}>();
if (!data || !data.id) {
#if (${table.templateType} == 2)## 树表特有
// 设置上级
await formApi.setValues(data);
#end
return;
}
if (data.id) {
modalApi.lock();
try {
data = await get${simpleClassName}(data.id);
formData.value = await get${simpleClassName}(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
// 设置到 values
formData.value = data;
await formApi.setValues(formData.value);
},
});
</script>
@@ -135,8 +138,8 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
<!-- 子表的表单 -->
<Tabs v-model:active-key="subTabsName">
#foreach ($subTable in $subTables)
@@ -149,6 +152,6 @@ const [Modal, modalApi] = useVbenModal({
</Tabs.TabPane>
#end
</Tabs>
#end
#end
</Modal>
</template>

View File

@@ -1,56 +1,67 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { Page, useVbenModal } from '@vben/common-ui';
import { message,Tabs } from 'ant-design-vue';
import { ref } from 'vue';
import {#if ($table.templateType != 2 && $deleteBatchEnable) confirm,#end Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart#if ($table.templateType != 2 && $deleteBatchEnable), isEmpty#end } from '@vben/utils';
import { message#if ($table.templateType == 11), Tabs#end } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
#if (${table.templateType} == 2)## 树表接口
import {
delete${simpleClassName},
export${simpleClassName},
get${simpleClassName}List,
} from '#/api/${table.moduleName}/${table.businessName}';
#else## 标准表接口
import {
delete${simpleClassName},#if ($deleteBatchEnable)
delete${simpleClassName}List,#end
export${simpleClassName},
get${simpleClassName}Page,
} from '#/api/${table.moduleName}/${table.businessName}';
#end
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
## 特殊:主子表专属逻辑
#if ( $table.templateType == 11 || $table.templateType == 12 )
#foreach ($subSimpleClassName in $subSimpleClassNames)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
import ${subSimpleClassName}List from './modules/${subSimpleClassName_strikeCase}-list.vue'
import ${subSimpleClassName}List from './modules/${subSimpleClassName_strikeCase}-list.vue';
#end
#end
import { ref, computed } from 'vue';
import { $t } from '#/locales';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
#if (${table.templateType} == 2)## 树表接口
import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
#else## 标准表接口
import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
#end
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import { useGridColumns, useGridFormSchema } from './data';
#if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况
/** 子表的列表 */
const subTabsName = ref('$subClassNameVars.get(0)')
#if ($table.templateType == 11)
const select${simpleClassName} = ref<${simpleClassName}Api.${simpleClassName}>();
const select${simpleClassName} = ref<${apiName}.${simpleClassName}>();
#end
#end
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
#if (${table.templateType} == 2)## 树表特有:控制表格展开收缩
/** 切换树形展开/收缩状态 */
const isExpanded = ref(true);
function toggleExpand() {
function handleExpand() {
isExpanded.value = !isExpanded.value;
gridApi.grid.setAllTreeExpand(isExpanded.value);
}
#end
/** 刷新表格 */
function onRefresh() {
function handleRefresh() {
#if ($table.templateType == 12) ## 内嵌情况
gridApi.reload();
#else
@@ -60,34 +71,31 @@ function onRefresh() {
/** 创建${table.classComment} */
function handleCreate() {
formModalApi.setData({}).open();
formModalApi.setData(null).open();
}
/** 编辑${table.classComment} */
function handleEdit(row: ${simpleClassName}Api.${simpleClassName}) {
formModalApi.setData(row).open();
}
#if (${table.templateType} == 2)## 树表特有:新增下级
/** 新增下级${table.classComment} */
function handleAppend(row: ${simpleClassName}Api.${simpleClassName}) {
/** 添加下级${table.classComment} */
function handleAppend(row: ${apiName}.${simpleClassName}) {
formModalApi.setData({ ${treeParentColumn.javaField}: row.id }).open();
}
#end
/** 编辑${table.classComment} */
function handleEdit(row: ${apiName}.${simpleClassName}) {
formModalApi.setData(row).open();
}
/** 删除${table.classComment} */
async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
async function handleDelete(row: ${apiName}.${simpleClassName}) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_key_msg',
duration: 0,
});
try {
await delete${simpleClassName}(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
await delete${simpleClassName}(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
handleRefresh();
} finally {
hideLoading();
}
@@ -96,30 +104,28 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
#if ($table.templateType != 2 && $deleteBatchEnable)
/** 批量删除${table.classComment} */
async function handleDeleteBatch() {
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
key: 'action_key_msg',
content: $t('ui.actionMessage.deletingBatch'),
duration: 0,
});
try {
await delete${simpleClassName}List(checkedIds.value);
checkedIds.value = [];
message.success({
content: $t('ui.actionMessage.deleteSuccess'),
key: 'action_key_msg',
});
onRefresh();
message.success($t('ui.actionMessage.deleteSuccess'));
handleRefresh();
} finally {
hideLoading();
}
}
const checkedIds = ref<number[]>([])
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
records,
}: {
records: ${simpleClassName}Api.${simpleClassName}[];
records: ${apiName}.${simpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
@@ -141,20 +147,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
height: 'auto',
#end
#if (${table.templateType} == 2)## 树表设置
treeConfig: {
parentField: '${treeParentColumn.javaField}',
rowField: 'id',
transform: true,
expandAll: true,
reserve: true,
},
pagerConfig: {
enabled: false,
},
#else## 标准表设置
pagerConfig: {
enabled: true,
},
keepSource: true,
#end
proxyConfig: {
ajax: {
@@ -184,27 +181,35 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true,
search: true,
},
} as VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>,
#if (${table.templateType} == 11 || $deleteBatchEnable)
gridEvents:{
#if (${table.templateType} == 2)## 树表设置
treeConfig: {
parentField: '${treeParentColumn.javaField}',
rowField: 'id',
transform: true,
expandAll: true,
reserve: true,
},
#end
} as VxeTableGridOptions<${apiName}.${simpleClassName}>,
#if (${table.templateType} == 11 || (${table.templateType} != 2 && $deleteBatchEnable))
gridEvents: {
#if(${table.templateType} == 11)
cellClick: ({ row }: { row: ${simpleClassName}Api.${simpleClassName}}) => {
cellClick: ({ row }: { row: ${apiName}.${simpleClassName}}) => {
select${simpleClassName}.value = row;
},
#end
#if(${table.templateType} != 2 && $deleteBatchEnable)
#if (${table.templateType} != 2 && $deleteBatchEnable)
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
#end
}
},
#end
});
</script>
<template>
<Page auto-content-height>
<FormModal @success="onRefresh" />
<FormModal @success="handleRefresh" />
#if ($table.templateType == 11) ## erp情况
<div>
#end
@@ -228,13 +233,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
<template #toolbar-tools>
<TableAction
:actions="[
#if (${table.templateType} == 2)## 树表特有:展开/收缩按钮
{
label: isExpanded ? '收缩' : '展开',
type: 'primary',
onClick: toggleExpand,
},
#end
{
label: $t('ui.actionTitle.create', ['${table.classComment}']),
type: 'primary',
@@ -242,6 +240,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'],
onClick: handleCreate,
},
#if (${table.templateType} == 2)## 树表特有:展开/收缩按钮
{
label: isExpanded ? '收缩' : '展开',
type: 'primary',
onClick: handleExpand,
},
#end
{
label: $t('ui.actionTitle.export'),
type: 'primary',
@@ -255,8 +260,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
type: 'primary',
danger: true,
icon: ACTION_ICON.DELETE,
disabled: isEmpty(checkedIds),
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'],
disabled: isEmpty(checkedIds),
onClick: handleDeleteBatch,
},
#end
@@ -297,7 +302,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
/>
</template>
</Grid>
#if ($table.templateType == 11) ## erp情况
<!-- 子表的表单 -->
<Tabs v-model:active-key="subTabsName" class="mt-2">

View File

@@ -2,8 +2,9 @@
#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
@@ -16,7 +17,7 @@
import { use${subSimpleClassName}FormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<${simpleClassName}Api.${subSimpleClassName}>();
const formData = ref<${apiName}.${subSimpleClassName}>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['${subTable.classComment}'])
@@ -42,10 +43,9 @@
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ${simpleClassName}Api.${subSimpleClassName};
const data = (await formApi.getValues()) as ${apiName}.${subSimpleClassName};
data.${subJoinColumn.javaField} = formData.value?.${subJoinColumn.javaField};
try {
await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data));
@@ -62,9 +62,8 @@
formData.value = undefined;
return;
}
// 加载数据
let data = modalApi.getData<${simpleClassName}Api.${subSimpleClassName}>();
let data = modalApi.getData<${apiName}.${subSimpleClassName}>();
if (!data) {
return;
}

View File

@@ -4,10 +4,12 @@
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
#set ($subClassNameVar = $subClassNameVars.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { computed, ref, h, onMounted,watch,nextTick } from 'vue';
import { $t } from '#/locales';
#if ($subTable.subJoinMany) ## 一对多
@@ -33,10 +35,10 @@ const props = defineProps<{
function onActionClick({
code,
row,
}: OnActionClickParams<${simpleClassName}Api.${subSimpleClassName}>) {
}: OnActionClickParams<${apiName}.${subSimpleClassName}>) {
switch (code) {
case 'delete': {
onDelete(row);
handleDelete(row);
break;
}
}
@@ -62,21 +64,21 @@ gridOptions: {
});
/** 添加${subTable.classComment} */
const onAdd = async () => {
await gridApi.grid.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1);
const handleAdd = async () => {
await gridApi.grid.insertAt({} as ${apiName}.${subSimpleClassName}, -1);
}
/** 删除${subTable.classComment} */
const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => {
const handleDelete = async (row: ${apiName}.${subSimpleClassName}) => {
await gridApi.grid.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): ${simpleClassName}Api.${subSimpleClassName}[] => {
const data = gridApi.grid.getData() as ${simpleClassName}Api.${subSimpleClassName}[];
const removeRecords = gridApi.grid.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
const insertRecords = gridApi.grid.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
getData: (): ${apiName}.${subSimpleClassName}[] => {
const data = gridApi.grid.getData() as ${apiName}.${subSimpleClassName}[];
const removeRecords = gridApi.grid.getRemoveRecords() as ${apiName}.${subSimpleClassName}[];
const insertRecords = gridApi.grid.getInsertRecords() as ${apiName}.${subSimpleClassName}[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
@@ -186,7 +188,7 @@ watch(
#end
</Grid>
<div class="flex justify-center -mt-4">
<Button :icon="h(Plus)" type="primary" ghost @click="onAdd" v-access:code="['${subTable.moduleName}:${simpleClassName_strikeCase}:create']">
<Button :icon="h(Plus)" type="primary" ghost @click="handleAdd" v-access:code="['${subTable.moduleName}:${simpleClassName_strikeCase}:create']">
{{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }}
</Button>
</div>

View File

@@ -5,14 +5,15 @@
#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
#if ($table.templateType == 11) ## erp
import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
#end
import { useVbenModal } from '@vben/common-ui';
import { confirm, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ref, computed, nextTick,watch } from 'vue';
import { $t } from '#/locales';
@@ -51,23 +52,20 @@ function handleCreate() {
}
/** 编辑${subTable.classComment} */
function handleEdit(row: ${simpleClassName}Api.${subSimpleClassName}) {
function handleEdit(row: ${apiName}.${subSimpleClassName}) {
formModalApi.setData(row).open();
}
/** 删除${subTable.classComment} */
async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
async function handleDelete(row: ${apiName}.${subSimpleClassName}) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_process_msg',
duration: 0,
});
try {
await delete${subSimpleClassName}(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
await delete${subSimpleClassName}(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
handleRefresh();
} finally {
hideLoading();
}
@@ -76,18 +74,16 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
#if ($deleteBatchEnable)
/** 批量删除${subTable.classComment} */
async function handleDeleteBatch() {
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
key: 'action_key_msg',
content: $t('ui.actionMessage.deletingBatch'),
duration: 0,
});
try {
await delete${subSimpleClassName}List(checkedIds.value);
checkedIds.value = [];
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
handleRefresh();
} finally {
hideLoading();
}
@@ -97,9 +93,9 @@ const checkedIds = ref<number[]>([])
function handleRowCheckboxChange({
records,
}: {
records: ${simpleClassName}Api.${subSimpleClassName}[];
records: ${apiName}.${subSimpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
@@ -149,7 +145,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
keyField: 'id',
isHover: true,
},
} as VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>,
} as VxeTableGridOptions<${apiName}.${subSimpleClassName}>,
#if (${table.templateType} == 11 && $deleteBatchEnable)
gridEvents:{
checkboxAll: handleRowCheckboxChange,
@@ -159,7 +155,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
});
/** 刷新表格 */
async function onRefresh() {
async function handleRefresh() {
#if ($table.templateType == 11) ## erp
await gridApi.query();
#else
@@ -179,7 +175,7 @@ watch(
return;
}
await nextTick();
await onRefresh()
await handleRefresh()
},
{ immediate: true },
);
@@ -187,7 +183,7 @@ watch(
<template>
#if ($table.templateType == 11) ## erp
<FormModal @success="onRefresh" />
<FormModal @success="handleRefresh" />
<Grid table-title="${subTable.classComment}列表">
<template #toolbar-tools>
<TableAction

View File

@@ -98,7 +98,7 @@ export function delete${simpleClassName}List(ids: number[]) {
/** 导出${table.classComment} */
export function export${simpleClassName}(params: any) {
return requestClient.download('${baseURL}/export-excel', params);
return requestClient.download('${baseURL}/export-excel', { params });
}
## 特殊:主子表专属逻辑

View File

@@ -3,10 +3,11 @@ import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleCl
import type { FormRules } from 'element-plus';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker, ElTreeSelect } from 'element-plus';
import { DICT_TYPE, getDictOptions } from '#/utils';
#if($table.templateType == 2)## 树表需要导入这些
import { get${simpleClassName}List } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { handleTree } from '@vben/utils'

View File

@@ -1,18 +1,18 @@
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { ref, h, reactive, onMounted, nextTick } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { cloneDeep, formatDateTime } from '@vben/utils';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
import { cloneDeep, downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
import { ElButton, ElMessage, ElLoading, ElTabs, ElTabPane, ElPagination, ElForm, ElFormItem, ElDatePicker, ElSelect, ElOption, ElInput } from 'element-plus';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
import ${simpleClassName}Form from './modules/form.vue';
import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
import { ContentWrap } from '#/components/content-wrap';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { TableToolbar } from '#/components/table-toolbar';
import { useTableToolbar } from '#/hooks';
## 特殊:主子表专属逻辑
#if ( $table.templateType == 11 || $table.templateType == 12 )
@@ -23,7 +23,6 @@ import { useTableToolbar } from '#/hooks';
#end
#end
import { ref, h, reactive,onMounted,nextTick } from 'vue';
import { $t } from '#/locales';
#if (${table.templateType} == 2)## 树表接口
import { handleTree,isEmpty } from '@vben/utils'
@@ -104,7 +103,7 @@ const getList = async () => {
}
/** 搜索按钮操作 */
const handleQuery = () => {
function handleQuery() {
#if ( $table.templateType != 2 )
queryParams.pageNo = 1
#end
@@ -112,7 +111,7 @@ const handleQuery = () => {
}
/** 重置按钮操作 */
const resetQuery = () => {
function resetQuery() {
queryFormRef.value.resetFields()
handleQuery()
}
@@ -124,7 +123,7 @@ const [FormModal, formModalApi] = useVbenModal({
/** 创建${table.classComment} */
function handleCreate() {
formModalApi.setData({}).open();
formModalApi.setData(null).open();
}
/** 编辑${table.classComment} */
@@ -143,7 +142,6 @@ function handleAppend(row: ${simpleClassName}Api.${simpleClassName}) {
async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.id]),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${simpleClassName}(row.id as number);
@@ -159,7 +157,6 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
async function handleDeleteBatch() {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting'),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${simpleClassName}List(checkedIds.value);
@@ -177,12 +174,12 @@ function handleRowCheckboxChange({
}: {
records: ${simpleClassName}Api.${simpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
/** 导出表格 */
async function onExport() {
async function handleExport() {
try {
exportLoading.value = true;
const data = await export${simpleClassName}(queryParams);
@@ -195,7 +192,7 @@ try {
#if (${table.templateType} == 2)
/** 切换树形展开/收缩状态 */
const isExpanded = ref(true);
function toggleExpand() {
function handleExpand() {
isExpanded.value = !isExpanded.value;
tableRef.value?.setAllTreeExpand(isExpanded.value);
}
@@ -306,7 +303,7 @@ onMounted(() => {
v-model:hidden-search="hiddenSearchBar"
>
#if (${table.templateType} == 2)
<el-button @click="toggleExpand" class="mr-2">
<el-button @click="handleExpand" class="mr-2">
{{ isExpanded ? '收缩' : '展开' }}
</el-button>
#end
@@ -324,7 +321,7 @@ onMounted(() => {
type="primary"
class="ml-2"
:loading="exportLoading"
@click="onExport"
@click="handleExport"
v-access:code="['${permissionPrefix}:export']"
>
{{ $t('ui.actionTitle.export') }}

View File

@@ -6,13 +6,15 @@
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { FormRules } from 'element-plus';
import { computed, ref, reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker, ElTreeSelect } from 'element-plus';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { computed, ref, reactive } from 'vue';
import { $t } from '#/locales';
import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';

View File

@@ -5,12 +5,15 @@
#set ($subClassNameVar = $subClassNameVars.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElButton, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker } from 'element-plus';
import { computed, ref, reactive, h, onMounted,watch,nextTick } from 'vue';
import { $t } from '#/locales';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { computed, ref, reactive, h, onMounted,watch,nextTick } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElButton, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker } from 'element-plus';
import { $t } from '#/locales';
#if ($subTable.subJoinMany) ## 一对多
import type { VxeTableInstance } from '#/adapter/vxe-table';
@@ -31,12 +34,12 @@ const props = defineProps<{
const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据
const tableRef = ref<VxeTableInstance>();
/** 添加${subTable.classComment} */
const onAdd = async () => {
async function handleAdd() {
await tableRef.value?.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1);
}
/** 删除${subTable.classComment} */
const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => {
async function onDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
await tableRef.value?.remove(row);
}
@@ -46,9 +49,11 @@ defineExpose({
const data = list.value as ${simpleClassName}Api.${subSimpleClassName}[];
const removeRecords = tableRef.value?.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
const insertRecords = tableRef.value?.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
?.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
return [
...data.filter(
(row) => !removeRecords.some((removed) => removed.id === row.id),
),
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
},
});
@@ -227,7 +232,7 @@ watch(
</vxe-column>
</vxe-table>
<div class="flex justify-center mt-4">
<el-button :icon="h(Plus)" type="primary" plain @click="onAdd" v-access:code="['${permissionPrefix}:create']">
<el-button :icon="h(Plus)" type="primary" plain @click="handleAdd" v-access:code="['${permissionPrefix}:create']">
{{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }}
</el-button>
</div>

View File

@@ -9,23 +9,25 @@
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';
import { cloneDeep, formatDateTime } from '@vben/utils';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { DictTag } from '#/components/dict-tag';
import { getRangePickerDefaultProps } from '#/utils';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { ContentWrap } from '#/components/content-wrap';
#if ($table.templateType == 11) ## erp
import { useVbenModal } from '@vben/common-ui';
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { ElMessage, ElLoading, ElButton, ElTabs, ElTabPane, ElPagination, ElForm, ElFormItem, ElDatePicker, ElSelect, ElOption, ElInput } from 'element-plus';
import { Plus, Trash2 } from '@vben/icons';
import { $t } from '#/locales';
import { TableToolbar } from '#/components/table-toolbar';
import { useTableToolbar } from '#/hooks';
#end
#if ($table.templateType == 11) ## erp
@@ -67,7 +69,6 @@ function handleEdit(row: ${simpleClassName}Api.${subSimpleClassName}) {
async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.id]),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${subSimpleClassName}(row.id as number);
@@ -83,7 +84,6 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
async function handleDeleteBatch() {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting'),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${subSimpleClassName}List(checkedIds.value);
@@ -101,7 +101,7 @@ function handleRowCheckboxChange({
}: {
records: ${simpleClassName}Api.${subSimpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
#end
@@ -129,19 +129,19 @@ function handleRowCheckboxChange({
})
/** 搜索按钮操作 */
const handleQuery = () => {
function handleQuery() {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
function resetQuery() {
queryFormRef.value.resetFields()
handleQuery()
}
#end
/** 查询列表 */
const getList = async () => {
/** 查询列表 */
async function getList() {
loading.value = true
try {
if (!props.${subJoinColumn.javaField}){
@@ -173,10 +173,10 @@ const resetQuery = () => {
} finally {
loading.value = false
}
}
}
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
if (!val) {
@@ -186,7 +186,7 @@ const resetQuery = () => {
await getList()
},
{ immediate: true },
);
);
#if ($table.templateType == 11) ## erp
/** 初始化 */
@@ -290,7 +290,7 @@ onMounted(() => {
<!-- 列表 -->
<ContentWrap title="${table.classComment}">
<template #extra>
<TableToolbar
<VbenVxeTableToolbar
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>

View File

@@ -1,10 +1,13 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
#if ( $table.templateType != 2 )
import type { PageParam, PageResult } from '@vben/request';
#end
import type { Dayjs } from 'dayjs';
import { requestClient } from '#/api/request';
#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
export namespace ${simpleClassName}Api {
export namespace ${apiName} {
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
@@ -60,27 +63,35 @@ export namespace ${simpleClassName}Api {
#if ( $table.templateType != 2 )
/** 查询${table.classComment}分页 */
export function get${simpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${simpleClassName}>>('${baseURL}/page', { params });
return requestClient.get<PageResult<${apiName}.${simpleClassName}>>(
'${baseURL}/page',
{ params },
);
}
#else
/** 查询${table.classComment}列表 */
export function get${simpleClassName}List(params: any) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}[]>('${baseURL}/list', { params });
return requestClient.get<${apiName}.${simpleClassName}[]>(
'${baseURL}/list',
{ params },
);
}
#end
/** 查询${table.classComment}详情 */
export function get${simpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}>(`${baseURL}/get?id=${id}`);
return requestClient.get<${apiName}.${simpleClassName}>(
`${baseURL}/get?id=${id}`,
);
}
/** 新增${table.classComment} */
export function create${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
export function create${simpleClassName}(data: ${apiName}.${simpleClassName}) {
return requestClient.post('${baseURL}/create', data);
}
/** 修改${table.classComment} */
export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
export function update${simpleClassName}(data: ${apiName}.${simpleClassName}) {
return requestClient.put('${baseURL}/update', data);
}
@@ -88,17 +99,19 @@ export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleCla
export function delete${simpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/delete?id=${id}`);
}
#if ( $table.templateType != 2 && $deleteBatchEnable)
/** 批量删除${table.classComment} */
export function delete${simpleClassName}List(ids: number[]) {
return requestClient.delete(`${baseURL}/delete-list?ids=${ids.join(',')}`)
return requestClient.delete(
`${baseURL}/delete-list?ids=${ids.join(',')}`,
);
}
#end
/** 导出${table.classComment} */
export function export${simpleClassName}(params: any) {
return requestClient.download('${baseURL}/export-excel', params);
return requestClient.download('${baseURL}/export-excel', { params });
}
## 特殊:主子表专属逻辑
@@ -118,31 +131,38 @@ export function export${simpleClassName}(params: any) {
#if ( $table.templateType == 11 )
/** 获得${subTable.classComment}分页 */
export function get${subSimpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${subSimpleClassName}>>(`${baseURL}/${subSimpleClassName_strikeCase}/page`, { params });
return requestClient.get<PageResult<${apiName}.${subSimpleClassName}>>(
`${baseURL}/${subSimpleClassName_strikeCase}/page`,
{ params },
);
}
## 情况二:非 MASTER_ERP 时,需要列表查询子表
#else
#if ( $subTable.subJoinMany )
/** 获得${subTable.classComment}列表 */
export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}[]>(`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
return requestClient.get<${apiName}.${subSimpleClassName}[]>(
`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`,
);
}
#else
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
return requestClient.get<${apiName}.${subSimpleClassName}>(
`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`,
);
}
#end
#end
## 特殊MASTER_ERP 时,支持单个的新增、修改、删除操作
#if ( $table.templateType == 11 )
/** 新增${subTable.classComment} */
export function create${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
export function create${subSimpleClassName}(data: ${apiName}.${subSimpleClassName}) {
return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data);
}
/** 修改${subTable.classComment} */
export function update${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
export function update${subSimpleClassName}(data: ${apiName}.${subSimpleClassName}) {
return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data);
}
@@ -154,13 +174,17 @@ export function delete${subSimpleClassName}(id: number) {
#if ($deleteBatchEnable)
/** 批量删除${subTable.classComment} */
export function delete${subSimpleClassName}List(ids: number[]) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`)
return requestClient.delete(
`${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`,
);
}
#end
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`);
return requestClient.get<${apiName}.${subSimpleClassName}>(
`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`,
);
}
#end
#end

View File

@@ -1,18 +1,17 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { z } from '#/adapter/form';
import {
DICT_TYPE,
getDictOptions,
getRangePickerDefaultProps,
} from '#/utils';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
#if(${table.templateType} == 2)## 树表需要导入这些
import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}';
import { handleTree } from '@vben/utils';
#end
import { getRangePickerDefaultProps } from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@@ -30,7 +29,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '上级${table.classComment}',
component: 'ApiTreeSelect',
componentProps: {
allowClear: true,
clearable: true,
api: async () => {
const data = await get${simpleClassName}List({});
data.unshift({
@@ -125,8 +124,9 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber',
componentProps: {
min: 0,
controlsPosition: 'right',
placeholder: '请输入${comment}',
controlsPosition: 'right',
class: '!w-full',
},
#end
},
@@ -158,13 +158,13 @@ export function useGridFormSchema(): VbenFormSchema[] {
#if ($column.htmlType == "input" || $column.htmlType == "textarea" || $column.htmlType == "editor")
component: 'Input',
componentProps: {
allowClear: true,
clearable: true,
placeholder: '请输入${comment}',
},
#elseif ($column.htmlType == "select" || $column.htmlType == "radio")
component: 'Select',
componentProps: {
allowClear: true,
clearable: true,
#if ("" != $dictType)## 设置了 dictType 数据字典的情况
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else## 未设置 dictType 数据字典的情况
@@ -176,7 +176,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
clearable: true,
},
#end
},
@@ -186,7 +186,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>['columns'] {
export function useGridColumns(): VxeTableGridOptions<${apiName}.${simpleClassName}>['columns'] {
return [
#if ($table.templateType != 2 && $deleteBatchEnable)
{ type: 'checkbox', width: 40 },
@@ -327,8 +327,9 @@ export function use${subSimpleClassName}FormSchema(): VbenFormSchema[] {
component: 'InputNumber',
componentProps: {
min: 0,
controlsPosition: 'right',
placeholder: '请输入${comment}',
controlsPosition: 'right',
class: '!w-full',
},
#end
},
@@ -361,13 +362,13 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] {
#if ($column.htmlType == "input" || $column.htmlType == "textarea" || $column.htmlType == "editor")
component: 'Input',
componentProps: {
allowClear: true,
clearable: true,
placeholder: '请输入${comment}',
},
#elseif ($column.htmlType == "select" || $column.htmlType == "radio")
component: 'Select',
componentProps: {
allowClear: true,
clearable: true,
#if ("" != $dictType)## 设置了 dictType 数据字典的情况
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else## 未设置 dictType 数据字典的情况
@@ -379,7 +380,7 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] {
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
clearable: true,
},
#end
},
@@ -389,7 +390,7 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] {
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
return [
#if ($table.templateType != 2 && $deleteBatchEnable)
{ type: 'checkbox', width: 40 },
@@ -426,7 +427,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${sim
#else
#if ($subTable.subJoinMany) ## 一对多
/** 新增/修改列表的字段 */
export function use${subSimpleClassName}GridEditColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] {
export function use${subSimpleClassName}GridEditColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
return [
#foreach($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
@@ -561,8 +562,9 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${sim
component: 'InputNumber',
componentProps: {
min: 0,
controlsPosition: 'right',
placeholder: '请输入${comment}',
controlsPosition: 'right',
class: '!w-full',
},
#end
},

View File

@@ -1,8 +1,16 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage, ElTabs, ElTabPane, ElCheckbox, ElInput, ElSelect, ElRadioGroup, ElCheckboxGroup, ElDatePicker } from 'element-plus';
import { ElMessage#if ($table.templateType == 11), ElTabs, ElTabPane#end } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
import { $t } from '#/locales';
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#foreach ($subSimpleClassName in $subSimpleClassNames)
@@ -12,21 +20,15 @@ import { ElMessage, ElTabs, ElTabPane, ElCheckbox, ElInput, ElSelect, ElRadioGro
#end
#end
import { computed, ref } from 'vue';
import { $t } from '#/locales';
import { useVbenForm } from '#/adapter/form';
import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<${simpleClassName}Api.${simpleClassName}>();
const formData = ref<${apiName}.${simpleClassName}>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['${table.classComment}'])
: $t('ui.actionTitle.create', ['${table.classComment}']);
});
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#if ( $subTables && $subTables.size() > 0 )
@@ -51,7 +53,7 @@ const [Form, formApi] = useVbenForm({
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
@@ -60,8 +62,8 @@ const [Modal, modalApi] = useVbenModal({
if (!valid) {
return;
}
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#if ( $subTables && $subTables.size() > 0 )
// 校验子表单
#foreach ($subTable in $subTables)
@@ -78,12 +80,12 @@ const [Modal, modalApi] = useVbenModal({
#end
#end
#end
#end
#end
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ${simpleClassName}Api.${simpleClassName};
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
const data = (await formApi.getValues()) as ${apiName}.${simpleClassName};
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#if ( $subTables && $subTables.size() > 0 )
// 拼接子表的数据
#foreach ($subTable in $subTables)
@@ -96,7 +98,7 @@ const [Modal, modalApi] = useVbenModal({
#end
#end
#end
#end
#end
try {
await (formData.value?.id ? update${simpleClassName}(data) : create${simpleClassName}(data));
// 关闭并提示
@@ -113,21 +115,22 @@ const [Modal, modalApi] = useVbenModal({
return;
}
// 加载数据
let data = modalApi.getData<${simpleClassName}Api.${simpleClassName}>();
if (!data) {
const data = modalApi.getData<${apiName}.${simpleClassName}>();
if (!data || !data.id) {
#if (${table.templateType} == 2)## 树表特有
// 设置上级
await formApi.setValues(data);
#end
return;
}
if (data.id) {
modalApi.lock();
try {
data = await get${simpleClassName}(data.id);
formData.value = await get${simpleClassName}(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
// 设置到 values
formData.value = data;
await formApi.setValues(formData.value);
},
});
</script>
@@ -135,20 +138,20 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName">
<ElTabs v-model="subTabsName">
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
<el-tab-pane name="$subClassNameVar" label="${subTable.classComment}">
<ElTabPane name="$subClassNameVar" label="${subTable.classComment}">
<${subSimpleClassName}Form ref="${subClassNameVar}FormRef" :${subJoinColumn_strikeCase}="formData?.id" />
</el-tab-pane>
#end
</el-tabs>
</ElTabPane>
#end
</ElTabs>
#end
</Modal>
</template>

View File

@@ -1,56 +1,67 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { Page, useVbenModal } from '@vben/common-ui';
import { ElMessage, ElTabs, ElTabPane, ElLoading } from 'element-plus';
import { ref } from 'vue';
import {#if ($table.templateType != 2 && $deleteBatchEnable) confirm,#end Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart#if ($table.templateType != 2 && $deleteBatchEnable), isEmpty#end } from '@vben/utils';
import { ElLoading, ElMessage#if ($table.templateType == 11), ElTabs, ElTabPane#end } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
#if (${table.templateType} == 2)## 树表接口
import {
delete${simpleClassName},
export${simpleClassName},
get${simpleClassName}List,
} from '#/api/${table.moduleName}/${table.businessName}';
#else## 标准表接口
import {
delete${simpleClassName},#if ($deleteBatchEnable)
delete${simpleClassName}List,#end
export${simpleClassName},
get${simpleClassName}Page,
} from '#/api/${table.moduleName}/${table.businessName}';
#end
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
## 特殊:主子表专属逻辑
#if ( $table.templateType == 11 || $table.templateType == 12 )
#foreach ($subSimpleClassName in $subSimpleClassNames)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
import ${subSimpleClassName}List from './modules/${subSimpleClassName_strikeCase}-list.vue'
import ${subSimpleClassName}List from './modules/${subSimpleClassName_strikeCase}-list.vue';
#end
#end
import { ref, computed } from 'vue';
import { $t } from '#/locales';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
#if (${table.templateType} == 2)## 树表接口
import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
#else## 标准表接口
import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
#end
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import { useGridColumns, useGridFormSchema } from './data';
#if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况
/** 子表的列表 */
const subTabsName = ref('$subClassNameVars.get(0)')
#if ($table.templateType == 11)
const select${simpleClassName} = ref<${simpleClassName}Api.${simpleClassName}>();
const select${simpleClassName} = ref<${apiName}.${simpleClassName}>();
#end
#end
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
#if (${table.templateType} == 2)## 树表特有:控制表格展开收缩
/** 切换树形展开/收缩状态 */
const isExpanded = ref(true);
function toggleExpand() {
function handleExpand() {
isExpanded.value = !isExpanded.value;
gridApi.grid.setAllTreeExpand(isExpanded.value);
}
#end
/** 刷新表格 */
function onRefresh() {
function handleRefresh() {
#if ($table.templateType == 12) ## 内嵌情况
gridApi.reload();
#else
@@ -60,31 +71,30 @@ function onRefresh() {
/** 创建${table.classComment} */
function handleCreate() {
formModalApi.setData({}).open();
formModalApi.setData(null).open();
}
/** 编辑${table.classComment} */
function handleEdit(row: ${simpleClassName}Api.${simpleClassName}) {
formModalApi.setData(row).open();
}
#if (${table.templateType} == 2)## 树表特有:新增下级
/** 新增下级${table.classComment} */
function handleAppend(row: ${simpleClassName}Api.${simpleClassName}) {
/** 添加下级${table.classComment} */
function handleAppend(row: ${apiName}.${simpleClassName}) {
formModalApi.setData({ ${treeParentColumn.javaField}: row.id }).open();
}
#end
/** 编辑${table.classComment} */
function handleEdit(row: ${apiName}.${simpleClassName}) {
formModalApi.setData(row).open();
}
/** 删除${table.classComment} */
async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
async function handleDelete(row: ${apiName}.${simpleClassName}) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.id]),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${simpleClassName}(row.id as number);
await delete${simpleClassName}(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id]));
onRefresh();
handleRefresh();
} finally {
loadingInstance.close();
}
@@ -93,27 +103,27 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
#if ($table.templateType != 2 && $deleteBatchEnable)
/** 批量删除${table.classComment} */
async function handleDeleteBatch() {
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting'),
background: 'rgba(0, 0, 0, 0.7)',
text: $t('ui.actionMessage.deletingBatch'),
});
try {
await delete${simpleClassName}List(checkedIds.value);
checkedIds.value = [];
ElMessage.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
handleRefresh();
} finally {
loadingInstance.close();
}
}
const checkedIds = ref<number[]>([])
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
records,
}: {
records: ${simpleClassName}Api.${simpleClassName}[];
records: ${apiName}.${simpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
@@ -135,20 +145,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
height: 'auto',
#end
#if (${table.templateType} == 2)## 树表设置
treeConfig: {
parentField: '${treeParentColumn.javaField}',
rowField: 'id',
transform: true,
expandAll: true,
reserve: true,
},
pagerConfig: {
enabled: false,
},
#else## 标准表设置
pagerConfig: {
enabled: true,
},
keepSource: true,
#end
proxyConfig: {
ajax: {
@@ -178,27 +179,35 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true,
search: true,
},
} as VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>,
#if (${table.templateType} == 11 || $deleteBatchEnable)
gridEvents:{
#if (${table.templateType} == 2)## 树表设置
treeConfig: {
parentField: '${treeParentColumn.javaField}',
rowField: 'id',
transform: true,
expandAll: true,
reserve: true,
},
#end
} as VxeTableGridOptions<${apiName}.${simpleClassName}>,
#if (${table.templateType} == 11 || (${table.templateType} != 2 && $deleteBatchEnable))
gridEvents: {
#if(${table.templateType} == 11)
cellClick: ({ row }: { row: ${simpleClassName}Api.${simpleClassName}}) => {
cellClick: ({ row }: { row: ${apiName}.${simpleClassName}}) => {
select${simpleClassName}.value = row;
},
#end
#if(${table.templateType} != 2 && $deleteBatchEnable)
#if (${table.templateType} != 2 && $deleteBatchEnable)
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
#end
}
},
#end
});
</script>
<template>
<Page auto-content-height>
<FormModal @success="onRefresh" />
<FormModal @success="handleRefresh" />
#if ($table.templateType == 11) ## erp情况
<div>
#end
@@ -206,29 +215,22 @@ const [Grid, gridApi] = useVbenVxeGrid({
#if ($table.templateType == 12) ## 内嵌情况
<template #expand_content="{ row }">
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName" class="mx-8">
<ElTabs v-model="subTabsName" class="mx-8">
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
<el-tab-pane name="$subClassNameVar" label="${subTable.classComment}">
<ElTabPane name="$subClassNameVar" label="${subTable.classComment}">
<${subSimpleClassName}List :${subJoinColumn_strikeCase}="row?.id" />
</el-tab-pane>
</ElTabPane>
#end
</el-tabs>
</ElTabs>
</template>
#end
<template #toolbar-tools>
<TableAction
:actions="[
#if (${table.templateType} == 2)## 树表特有:展开/收缩按钮
{
label: isExpanded ? '收缩' : '展开',
type: 'primary',
onClick: toggleExpand,
},
#end
{
label: $t('ui.actionTitle.create', ['${table.classComment}']),
type: 'primary',
@@ -236,6 +238,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'],
onClick: handleCreate,
},
#if (${table.templateType} == 2)## 树表特有:展开/收缩按钮
{
label: isExpanded ? '收缩' : '展开',
type: 'primary',
onClick: handleExpand,
},
#end
{
label: $t('ui.actionTitle.export'),
type: 'primary',
@@ -248,8 +257,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
label: $t('ui.actionTitle.deleteBatch'),
type: 'danger',
icon: ACTION_ICON.DELETE,
disabled: isEmpty(checkedIds),
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'],
disabled: isEmpty(checkedIds),
onClick: handleDeleteBatch,
},
#end
@@ -262,7 +271,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
#if (${table.templateType} == 2)## 树表特有:新增下级
{
label: '新增下级',
type: 'text',
type: 'primary',
link: true,
icon: ACTION_ICON.ADD,
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'],
onClick: handleAppend.bind(null, row),
@@ -270,7 +280,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
#end
{
label: $t('common.edit'),
type: 'text',
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:update'],
onClick: handleEdit.bind(null, row),
@@ -278,7 +289,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
{
label: $t('common.delete'),
type: 'danger',
text: true,
link: true,
icon: ACTION_ICON.DELETE,
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'],
popConfirm: {
@@ -290,20 +301,19 @@ const [Grid, gridApi] = useVbenVxeGrid({
/>
</template>
</Grid>
#if ($table.templateType == 11) ## erp情况
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName" class="mt-2">
<ElTabs v-model="subTabsName" class="mt-2">
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
<el-tab-pane name="$subClassNameVar" label="${subTable.classComment}">
<ElTabPane name="$subClassNameVar" label="${subTable.classComment}">
<${subSimpleClassName}List :${subJoinColumn_strikeCase}="select${simpleClassName}?.id" />
</el-tab-pane>
</ElTabPane>
#end
</el-tabs>
</ElTabs>
</div>
#end
</Page>

View File

@@ -2,21 +2,24 @@
#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { computed, ref } from 'vue';
import { $t } from '#/locales';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
import { $t } from '#/locales';
import { use${subSimpleClassName}FormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<${simpleClassName}Api.${subSimpleClassName}>();
const formData = ref<${apiName}.${subSimpleClassName}>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['${subTable.classComment}'])
@@ -45,7 +48,7 @@
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ${simpleClassName}Api.${subSimpleClassName};
const data = (await formApi.getValues()) as ${apiName}.${subSimpleClassName};
data.${subJoinColumn.javaField} = formData.value?.${subJoinColumn.javaField};
try {
await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data));
@@ -64,7 +67,7 @@
}
// 加载数据
let data = modalApi.getData<${simpleClassName}Api.${subSimpleClassName}>();
let data = modalApi.getData<${apiName}.${subSimpleClassName}>();
if (!data) {
return;
}

View File

@@ -4,10 +4,12 @@
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
#set ($subClassNameVar = $subClassNameVars.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { computed, ref, h, onMounted,watch,nextTick } from 'vue';
import { $t } from '#/locales';
#if ($subTable.subJoinMany) ## 一对多
@@ -33,7 +35,7 @@ const props = defineProps<{
function onActionClick({
code,
row,
}: OnActionClickParams<${simpleClassName}Api.${subSimpleClassName}>) {
}: OnActionClickParams<${apiName}.${subSimpleClassName}>) {
switch (code) {
case 'delete': {
onDelete(row);
@@ -67,16 +69,16 @@ const onAdd = async () => {
}
/** 删除${subTable.classComment} */
const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => {
const onDelete = async (row: ${apiName}.${subSimpleClassName}) => {
await gridApi.grid.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): ${simpleClassName}Api.${subSimpleClassName}[] => {
const data = gridApi.grid.getData() as ${simpleClassName}Api.${subSimpleClassName}[];
const removeRecords = gridApi.grid.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
const insertRecords = gridApi.grid.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
getData: (): ${apiName}.${subSimpleClassName}[] => {
const data = gridApi.grid.getData() as ${apiName}.${subSimpleClassName}[];
const removeRecords = gridApi.grid.getRemoveRecords() as ${apiName}.${subSimpleClassName}[];
const insertRecords = gridApi.grid.getInsertRecords() as ${apiName}.${subSimpleClassName}[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));

View File

@@ -5,9 +5,10 @@
#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
#if ($table.templateType == 11) ## erp
import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
@@ -41,6 +42,19 @@ const [FormModal, formModalApi] = useVbenModal({
destroyOnClose: true,
});
/** 刷新表格 */
async function handleRefresh() {
#if ($table.templateType == 11) ## erp
await gridApi.query();
#else
#if ($subTable.subJoinMany) ## 一对多
await gridApi.grid.loadData(await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!));
#else
await gridApi.grid.loadData([await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)]);
#end
#end
}
/** 创建${subTable.classComment} */
function handleCreate() {
if (!props.${subJoinColumn.javaField}){
@@ -51,20 +65,19 @@ function handleCreate() {
}
/** 编辑${subTable.classComment} */
function handleEdit(row: ${simpleClassName}Api.${subSimpleClassName}) {
function handleEdit(row: ${apiName}.${subSimpleClassName}) {
formModalApi.setData(row).open();
}
/** 删除${subTable.classComment} */
async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
async function handleDelete(row: ${apiName}.${subSimpleClassName}) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.id]),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${subSimpleClassName}(row.id as number);
await delete${subSimpleClassName}(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id]));
onRefresh();
handleRefresh();
} finally {
loadingInstance.close();
}
@@ -75,13 +88,12 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
async function handleDeleteBatch() {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting'),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${subSimpleClassName}List(checkedIds.value);
checkedIds.value = [];
ElMessage.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
handleRefresh();
} finally {
loadingInstance.close();
}
@@ -91,9 +103,9 @@ const checkedIds = ref<number[]>([])
function handleRowCheckboxChange({
records,
}: {
records: ${simpleClassName}Api.${subSimpleClassName}[];
records: ${apiName}.${subSimpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
@@ -143,7 +155,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
keyField: 'id',
isHover: true,
},
} as VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>,
} as VxeTableGridOptions<${apiName}.${subSimpleClassName}>,
#if (${table.templateType} == 11 && $deleteBatchEnable)
gridEvents:{
checkboxAll: handleRowCheckboxChange,
@@ -152,19 +164,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
#end
});
/** 刷新表格 */
async function onRefresh() {
#if ($table.templateType == 11) ## erp
await gridApi.query();
#else
#if ($subTable.subJoinMany) ## 一对多
await gridApi.grid.loadData(await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!));
#else
await gridApi.grid.loadData([await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)]);
#end
#end
}
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
@@ -173,7 +172,7 @@ watch(
return;
}
await nextTick();
await onRefresh()
await handleRefresh()
},
{ immediate: true },
);
@@ -181,7 +180,7 @@ watch(
<template>
#if ($table.templateType == 11) ## erp
<FormModal @success="onRefresh" />
<FormModal @success="handleRefresh" />
<Grid table-title="${subTable.classComment}列表">
<template #toolbar-tools>
<TableAction
@@ -211,7 +210,8 @@ watch(
:actions="[
{
label: $t('common.edit'),
type: 'text',
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:update'],
onClick: handleEdit.bind(null, row),
@@ -219,7 +219,7 @@ watch(
{
label: $t('common.delete'),
type: 'danger',
text: true,
link: true,
icon: ACTION_ICON.DELETE,
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'],
popConfirm: {

View File

@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
import lombok.Data;
/**
* IoT TCP 配置 {@link IotAbstractDataSinkConfig} 实现类
*
* @author HUIHUI
*/
@Data
public class IotDataSinkTcpConfig extends IotAbstractDataSinkConfig {
/**
* TCP 服务器地址
*/
private String host;
/**
* TCP 服务器端口
*/
private Integer port;
/**
* 连接超时时间(毫秒)
*/
private Integer connectTimeoutMs = 5000;
/**
* 读取超时时间(毫秒)
*/
private Integer readTimeoutMs = 10000;
/**
* 是否启用 SSL
*/
private Boolean ssl = false;
/**
* SSL 证书路径(当 ssl=true 时需要)
*/
private String sslCertPath;
/**
* 数据格式JSON 或 BINARY
*/
private String dataFormat = "JSON";
/**
* 心跳间隔时间毫秒0 表示不启用心跳
*/
private Long heartbeatIntervalMs = 30000L;
/**
* 重连间隔时间(毫秒)
*/
private Long reconnectIntervalMs = 5000L;
/**
* 最大重连次数
*/
private Integer maxReconnectAttempts = 3;
}

View File

@@ -0,0 +1,87 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
import lombok.Data;
/**
* IoT WebSocket 配置 {@link IotAbstractDataSinkConfig} 实现类
* <p>
* 配置设备消息通过 WebSocket 协议发送到外部 WebSocket 服务器
* 支持 WebSocket (ws://) 和 WebSocket Secure (wss://) 连接
*
* @author HUIHUI
*/
@Data
public class IotDataSinkWebSocketConfig extends IotAbstractDataSinkConfig {
/**
* WebSocket 服务器地址
* 例如ws://localhost:8080/ws 或 wss://example.com/ws
*/
private String serverUrl;
/**
* 连接超时时间(毫秒)
*/
private Integer connectTimeoutMs = 5000;
/**
* 发送超时时间(毫秒)
*/
private Integer sendTimeoutMs = 10000;
/**
* 心跳间隔时间毫秒0 表示不启用心跳
*/
private Long heartbeatIntervalMs = 30000L;
/**
* 心跳消息内容JSON 格式)
*/
private String heartbeatMessage = "{\"type\":\"heartbeat\"}";
/**
* 子协议列表(逗号分隔)
*/
private String subprotocols;
/**
* 自定义请求头JSON 格式)
*/
private String customHeaders;
/**
* 是否启用 SSL 证书验证(仅对 wss:// 生效)
*/
private Boolean verifySslCert = true;
/**
* 数据格式JSON 或 TEXT
*/
private String dataFormat = "JSON";
/**
* 重连间隔时间(毫秒)
*/
private Long reconnectIntervalMs = 5000L;
/**
* 最大重连次数
*/
private Integer maxReconnectAttempts = 3;
/**
* 是否启用压缩
*/
private Boolean enableCompression = false;
/**
* 消息发送重试次数
*/
private Integer sendRetryCount = 1;
/**
* 消息发送重试间隔(毫秒)
*/
private Long sendRetryIntervalMs = 1000L;
}

View File

@@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.iot.service.rule.data.action;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* TCP 的 {@link IotDataRuleAction} 实现类
* <p>
* 负责将设备消息发送到外部 TCP 服务器
* 支持普通 TCP 和 SSL TCP 连接,支持 JSON 和 BINARY 数据格式
* 使用连接池管理 TCP 连接,提高性能和资源利用率
*
* @author HUIHUI
*/
@Component
@Slf4j
public class IotTcpDataRuleAction extends
IotDataRuleCacheableAction<IotDataSinkTcpConfig, IotTcpClient> {
private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5);
private static final Duration SEND_TIMEOUT = Duration.ofSeconds(10);
@Override
public Integer getType() {
return IotDataSinkTypeEnum.TCP.getType();
}
@Override
protected IotTcpClient initProducer(IotDataSinkTcpConfig config) throws Exception {
// 1.1 参数校验
if (config.getHost() == null || config.getHost().trim().isEmpty()) {
throw new IllegalArgumentException("TCP 服务器地址不能为空");
}
if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
throw new IllegalArgumentException("TCP 服务器端口无效");
}
// 2.1 创建 TCP 客户端
IotTcpClient tcpClient = new IotTcpClient(
config.getHost(),
config.getPort(),
config.getConnectTimeoutMs(),
config.getReadTimeoutMs(),
config.getSsl(),
config.getSslCertPath(),
config.getDataFormat()
);
// 2.2 连接服务器
tcpClient.connect();
log.info("[initProducer][TCP 客户端创建并连接成功,服务器: {}:{}SSL: {},数据格式: {}]",
config.getHost(), config.getPort(), config.getSsl(), config.getDataFormat());
return tcpClient;
}
@Override
protected void closeProducer(IotTcpClient producer) throws Exception {
if (producer != null) {
producer.close();
}
}
@Override
protected void execute(IotDeviceMessage message, IotDataSinkTcpConfig config) throws Exception {
try {
// 1.1 获取或创建 TCP 客户端
IotTcpClient tcpClient = getProducer(config);
// 1.2 检查连接状态,如果断开则重新连接
if (!tcpClient.isConnected()) {
log.warn("[execute][TCP 连接已断开,尝试重新连接,服务器: {}:{}]", config.getHost(), config.getPort());
tcpClient.connect();
}
// 2.1 发送消息并等待结果
tcpClient.sendMessage(message);
// 2.2 记录发送成功日志
log.info("[execute][message({}) config({}) 发送成功TCP 服务器: {}:{}]",
message, config, config.getHost(), config.getPort());
} catch (Exception e) {
log.error("[execute][message({}) config({}) 发送失败TCP 服务器: {}:{}]",
message, config, config.getHost(), config.getPort(), e);
throw e;
}
}
}

View File

@@ -0,0 +1,184 @@
package cn.iocoder.yudao.module.iot.service.rule.data.action.tcp;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import lombok.extern.slf4j.Slf4j;
import javax.net.ssl.SSLSocketFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* IoT TCP 客户端
* <p>
* 负责与外部 TCP 服务器建立连接并发送设备消息
* 支持 JSON 和 BINARY 两种数据格式,支持 SSL 加密连接
*
* @author HUIHUI
*/
@Slf4j
public class IotTcpClient {
private final String host;
private final Integer port;
private final Integer connectTimeoutMs;
private final Integer readTimeoutMs;
private final Boolean ssl;
private final String sslCertPath;
private final String dataFormat;
private Socket socket;
private OutputStream outputStream;
private BufferedReader reader;
private final AtomicBoolean connected = new AtomicBoolean(false);
// TODO @puhui999default 值IotDataSinkTcpConfig.java 枚举起来哈;
public IotTcpClient(String host, Integer port, Integer connectTimeoutMs, Integer readTimeoutMs,
Boolean ssl, String sslCertPath, String dataFormat) {
this.host = host;
this.port = port;
this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : 5000;
this.readTimeoutMs = readTimeoutMs != null ? readTimeoutMs : 10000;
this.ssl = ssl != null ? ssl : false;
this.sslCertPath = sslCertPath;
this.dataFormat = dataFormat != null ? dataFormat : "JSON";
}
/**
* 连接到 TCP 服务器
*/
public void connect() throws Exception {
if (connected.get()) {
log.warn("[connect][TCP 客户端已经连接,无需重复连接]");
return;
}
try {
if (ssl) {
// SSL 连接
SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
socket = sslSocketFactory.createSocket();
} else {
// 普通连接
socket = new Socket();
}
// 连接服务器
socket.connect(new InetSocketAddress(host, port), connectTimeoutMs);
socket.setSoTimeout(readTimeoutMs);
// 获取输入输出流
outputStream = socket.getOutputStream();
reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
// 更新状态
connected.set(true);
log.info("[connect][TCP 客户端连接成功,服务器地址: {}:{}]", host, port);
} catch (Exception e) {
close();
log.error("[connect][TCP 客户端连接失败,服务器地址: {}:{}]", host, port, e);
throw e;
}
}
/**
* 发送设备消息
*
* @param message 设备消息
* @throws Exception 发送异常
*/
public void sendMessage(IotDeviceMessage message) throws Exception {
if (!connected.get()) {
throw new IllegalStateException("TCP 客户端未连接");
}
try {
// TODO @puhui999枚举值
String messageData;
if ("JSON".equalsIgnoreCase(dataFormat)) {
// JSON 格式
messageData = JsonUtils.toJsonString(message);
} else {
// BINARY 格式(这里简化为字符串,实际可能需要自定义二进制协议)
messageData = message.toString();
}
// 发送消息
outputStream.write(messageData.getBytes(StandardCharsets.UTF_8));
outputStream.write('\n'); // 添加换行符作为消息分隔符
outputStream.flush();
log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]",
message.getDeviceId(), messageData.length());
} catch (Exception e) {
log.error("[sendMessage][发送消息失败,设备 ID: {}]", message.getDeviceId(), e);
throw e;
}
}
/**
* 关闭连接
*/
public void close() {
if (!connected.get()) {
return;
}
try {
// 关闭资源
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
log.warn("[close][关闭输入流失败]", e);
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
log.warn("[close][关闭输出流失败]", e);
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
log.warn("[close][关闭 Socket 失败]", e);
}
}
// 更新状态
connected.set(false);
log.info("[close][TCP 客户端连接已关闭,服务器地址: {}:{}]", host, port);
} catch (Exception e) {
log.error("[close][关闭 TCP 客户端连接异常]", e);
}
}
/**
* 检查连接状态
*
* @return 是否已连接
*/
public boolean isConnected() {
return connected.get() && socket != null && !socket.isClosed();
}
@Override
public String toString() {
return "IotTcpClient{" +
"host='" + host + '\'' +
", port=" + port +
", ssl=" + ssl +
", dataFormat='" + dataFormat + '\'' +
", connected=" + connected.get() +
'}';
}
}

View File

@@ -0,0 +1,146 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.action;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
/**
* IoT 设备服务调用的 {@link IotSceneRuleAction} 实现类
*
* @author HUIHUI
*/
@Component
@Slf4j
public class IotDeviceServiceInvokeSceneRuleAction implements IotSceneRuleAction {
@Resource
private IotDeviceService deviceService;
@Resource
private IotDeviceMessageService deviceMessageService;
@Override
public void execute(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 参数校验
if (actionConfig.getDeviceId() == null) {
log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
return;
}
if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
log.error("[execute][规则场景({}) 动作配置({}) 服务标识符不能为空]", rule.getId(), actionConfig);
return;
}
// 2. 判断是否为全部设备
if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
executeForAllDevices(message, rule, actionConfig);
} else {
executeForSingleDevice(message, rule, actionConfig);
}
}
/**
* 为单个设备执行服务调用
*/
private void executeForSingleDevice(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 获取设备信息
IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
if (device == null) {
log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
rule.getId(), actionConfig, actionConfig.getDeviceId());
return;
}
// 2. 执行服务调用
executeServiceInvokeForDevice(rule, actionConfig, device);
}
/**
* 为产品下的所有设备执行服务调用
*/
private void executeForAllDevices(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 参数校验
if (actionConfig.getProductId() == null) {
log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
return;
}
// 2. 获取产品下的所有设备
List<IotDeviceDO> devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
if (CollUtil.isEmpty(devices)) {
log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
rule.getId(), actionConfig, actionConfig.getProductId());
return;
}
// 3. 遍历所有设备执行服务调用
for (IotDeviceDO device : devices) {
executeServiceInvokeForDevice(rule, actionConfig, device);
}
}
/**
* 为指定设备执行服务调用
*/
private void executeServiceInvokeForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
// 1. 构建服务调用消息
IotDeviceMessage downstreamMessage = buildServiceInvokeMessage(actionConfig, device);
if (downstreamMessage == null) {
log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 构建服务调用消息失败]",
rule.getId(), actionConfig, device.getId());
return;
}
// 2. 发送设备消息
try {
IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
log.info("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息({}) 发送成功]",
rule.getId(), actionConfig, device.getId(), result.getId());
} catch (Exception e) {
log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息发送失败]",
rule.getId(), actionConfig, device.getId(), e);
}
}
/**
* 构建服务调用消息
*
* @param actionConfig 动作配置
* @param device 设备信息
* @return 设备消息
*/
private IotDeviceMessage buildServiceInvokeMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
try {
// 服务调用参数格式: {"identifier": "serviceId", "params": {...}}
Object params = MapUtil.builder()
.put("identifier", actionConfig.getIdentifier())
.put("params", actionConfig.getParams() != null ? actionConfig.getParams() : Collections.emptyMap())
.build();
return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod(), params);
} catch (Exception e) {
log.error("[buildServiceInvokeMessage][构建服务调用消息异常]", e);
return null;
}
}
@Override
public IotSceneRuleActionTypeEnum getType() {
return IotSceneRuleActionTypeEnum.DEVICE_SERVICE_INVOKE;
}
}

View File

@@ -0,0 +1,154 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob;
import lombok.extern.slf4j.Slf4j;
import org.quartz.SchedulerException;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
/**
* IoT 场景规则定时触发器处理器:负责管理定时触发器的注册、更新、删除等操作
*
* @author HUIHUI
*/
@Component
@Slf4j
public class IotSceneRuleTimerHandler {
@Resource(name = "iotSchedulerManager")
private IotSchedulerManager schedulerManager;
/**
* 注册场景规则的定时触发器
*
* @param sceneRule 场景规则
*/
public void registerTimerTriggers(IotSceneRuleDO sceneRule) {
// 1. 过滤出定时触发器
if (sceneRule == null || CollUtil.isEmpty(sceneRule.getTriggers())) {
return;
}
List<IotSceneRuleDO.Trigger> timerTriggers = filterList(sceneRule.getTriggers(),
trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType()));
if (CollUtil.isEmpty(timerTriggers)) {
return;
}
// 2. 注册每个定时触发器
timerTriggers.forEach(trigger -> registerSingleTimerTrigger(sceneRule, trigger));
}
/**
* 更新场景规则的定时触发器
*
* @param sceneRule 场景规则
*/
public void updateTimerTriggers(IotSceneRuleDO sceneRule) {
if (sceneRule == null) {
return;
}
// 1. 先删除旧的定时任务
unregisterTimerTriggers(sceneRule.getId());
// 2.1 如果场景规则已禁用,则不重新注册
if (CommonStatusEnum.isDisable(sceneRule.getStatus())) {
log.info("[updateTimerTriggers][场景规则({}) 已禁用,不注册定时触发器]", sceneRule.getId());
return;
}
// 2.2 重新注册定时触发器
registerTimerTriggers(sceneRule);
}
/**
* 注销场景规则的定时触发器
*
* @param sceneRuleId 场景规则 ID
*/
public void unregisterTimerTriggers(Long sceneRuleId) {
if (sceneRuleId == null) {
return;
}
String jobName = buildJobName(sceneRuleId);
try {
schedulerManager.deleteJob(jobName);
log.info("[unregisterTimerTriggers][场景规则({}) 定时触发器注销成功]", sceneRuleId);
} catch (SchedulerException e) {
log.error("[unregisterTimerTriggers][场景规则({}) 定时触发器注销失败]", sceneRuleId, e);
}
}
/**
* 暂停场景规则的定时触发器
*
* @param sceneRuleId 场景规则 ID
*/
public void pauseTimerTriggers(Long sceneRuleId) {
if (sceneRuleId == null) {
return;
}
String jobName = buildJobName(sceneRuleId);
try {
schedulerManager.pauseJob(jobName);
log.info("[pauseTimerTriggers][场景规则({}) 定时触发器暂停成功]", sceneRuleId);
} catch (SchedulerException e) {
log.error("[pauseTimerTriggers][场景规则({}) 定时触发器暂停失败]", sceneRuleId, e);
}
}
/**
* 注册单个定时触发器
*
* @param sceneRule 场景规则
* @param trigger 定时触发器配置
*/
private void registerSingleTimerTrigger(IotSceneRuleDO sceneRule, IotSceneRuleDO.Trigger trigger) {
// 1. 参数校验
if (StrUtil.isBlank(trigger.getCronExpression())) {
log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器缺少 CRON 表达式]", sceneRule.getId());
return;
}
try {
// 2.1 构建任务名称和数据
String jobName = buildJobName(sceneRule.getId());
// 2.2 注册定时任务
schedulerManager.addOrUpdateJob(
IotSceneRuleJob.class,
jobName,
trigger.getCronExpression(),
IotSceneRuleJob.buildJobDataMap(sceneRule.getId())
);
log.info("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册成功CRON: {}]",
sceneRule.getId(), trigger.getCronExpression());
} catch (SchedulerException e) {
log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册失败CRON: {}]",
sceneRule.getId(), trigger.getCronExpression(), e);
}
}
/**
* 构建任务名称
*
* @param sceneRuleId 场景规则 ID
* @return 任务名称
*/
private String buildJobName(Long sceneRuleId) {
return "iot_scene_rule_timer_" + sceneRuleId;
}
}

View File

@@ -0,0 +1,162 @@
package cn.iocoder.yudao.module.iot.service.rule.data.action;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* {@link IotTcpDataRuleAction} 的单元测试
*
* @author HUIHUI
*/
class IotTcpDataRuleActionTest {
private IotTcpDataRuleAction tcpDataRuleAction;
@Mock
private IotTcpClient mockTcpClient;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
tcpDataRuleAction = new IotTcpDataRuleAction();
}
@Test
public void testGetType() {
// 准备参数
Integer expectedType = 2; // 数据接收类型枚举中 TCP 类型的值
// 调用方法
Integer actualType = tcpDataRuleAction.getType();
// 断言结果
assertEquals(expectedType, actualType);
}
// TODO @puhui999_ 后面是小写哈,单测的命名规则。
@Test
public void testInitProducer_Success() throws Exception {
// 准备参数
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
config.setHost("localhost");
config.setPort(8080);
config.setDataFormat("JSON");
config.setSsl(false);
// 调用方法 & 断言结果
// 此测试需要实际的 TCP 连接,在单元测试中可能不可用
// 目前我们只验证配置是否有效
assertNotNull(config.getHost());
assertTrue(config.getPort() > 0 && config.getPort() <= 65535);
}
@Test
public void testInitProducer_InvalidHost() {
// 准备参数
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
config.setHost("");
config.setPort(8080);
// 调用方法 & 断言结果
IotTcpDataRuleAction action = new IotTcpDataRuleAction();
// 测试验证逻辑(通常在 initProducer 方法中)
assertThrows(IllegalArgumentException.class, () -> {
if (config.getHost() == null || config.getHost().trim().isEmpty()) {
throw new IllegalArgumentException("TCP 服务器地址不能为空");
}
});
}
@Test
public void testInitProducer_InvalidPort() {
// 准备参数
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
config.setHost("localhost");
config.setPort(-1);
// 调用方法 & 断言结果
assertThrows(IllegalArgumentException.class, () -> {
if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
throw new IllegalArgumentException("TCP 服务器端口无效");
}
});
}
@Test
public void testCloseProducer() throws Exception {
// 准备参数
IotTcpClient client = mock(IotTcpClient.class);
// 调用方法
tcpDataRuleAction.closeProducer(client);
// 断言结果
verify(client, times(1)).close();
}
@Test
public void testExecute_WithValidConfig() {
// 准备参数
IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.report",
"{\"temperature\": 25.5, \"humidity\": 60}");
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
config.setHost("localhost");
config.setPort(8080);
config.setDataFormat("JSON");
// 调用方法 & 断言结果
// 通常这需要实际的 TCP 连接
// 在单元测试中,我们只验证输入参数
assertNotNull(message);
assertNotNull(config);
assertNotNull(config.getHost());
assertTrue(config.getPort() > 0);
}
@Test
public void testConfig_DefaultValues() {
// 准备参数
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
// 调用方法 & 断言结果
// 验证默认值
assertEquals("JSON", config.getDataFormat());
assertEquals(5000, config.getConnectTimeoutMs());
assertEquals(10000, config.getReadTimeoutMs());
assertEquals(false, config.getSsl());
assertEquals(30000L, config.getHeartbeatIntervalMs());
assertEquals(5000L, config.getReconnectIntervalMs());
assertEquals(3, config.getMaxReconnectAttempts());
}
@Test
public void testMessageSerialization() {
// 准备参数
IotDeviceMessage message = IotDeviceMessage.builder()
.deviceId(123L)
.method("thing.property.report")
.params("{\"temperature\": 25.5}")
.build();
// 调用方法
String json = JsonUtils.toJsonString(message);
// 断言结果
assertNotNull(json);
assertTrue(json.contains("\"deviceId\":123"));
assertTrue(json.contains("\"method\":\"thing.property.report\""));
assertTrue(json.contains("\"temperature\":25.5"));
}
}

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* Matcher 测试基类
* 提供通用的 Spring 测试配置
*
* @author HUIHUI
*/
@SpringJUnitConfig
public abstract class IotBaseConditionMatcherTest {
/**
* 注入一下 SpringUtil解析 EL 表达式时需要
* {@link SpringExpressionUtils#parseExpression}
*/
@Configuration
static class TestConfig {
@Bean
public SpringUtil springUtil() {
return new SpringUtil();
}
}
}

View File

@@ -0,0 +1,126 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.quartz.SchedulerException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
* {@link IotSceneRuleTimerHandler} 的单元测试类
*
* @author HUIHUI
*/
@ExtendWith(MockitoExtension.class)
public class IotSceneRuleTimerHandlerTest {
@Mock
private IotSchedulerManager schedulerManager;
@InjectMocks
private IotSceneRuleTimerHandler timerHandler;
@BeforeEach
void setUp() {
// 重置 Mock 对象
reset(schedulerManager);
}
@Test
public void testRegisterTimerTriggers_success() throws SchedulerException {
// 准备参数
Long sceneRuleId = 1L;
IotSceneRuleDO sceneRule = new IotSceneRuleDO();
sceneRule.setId(sceneRuleId);
sceneRule.setStatus(0); // 0 表示启用
// 创建定时触发器
IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger();
timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
timerTrigger.setCronExpression("0 0 12 * * ?"); // 每天中午12点
sceneRule.setTriggers(ListUtil.toList(timerTrigger));
// 调用
timerHandler.registerTimerTriggers(sceneRule);
// 验证
verify(schedulerManager, times(1)).addOrUpdateJob(
eq(IotSceneRuleJob.class),
eq("iot_scene_rule_timer_" + sceneRuleId),
eq("0 0 12 * * ?"),
eq(IotSceneRuleJob.buildJobDataMap(sceneRuleId))
);
}
@Test
public void testRegisterTimerTriggers_noTimerTrigger() throws SchedulerException {
// 准备参数 - 没有定时触发器
IotSceneRuleDO sceneRule = new IotSceneRuleDO();
sceneRule.setStatus(0); // 0 表示启用
// 创建非定时触发器
IotSceneRuleDO.Trigger deviceTrigger = new IotSceneRuleDO.Trigger();
deviceTrigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType());
sceneRule.setTriggers(ListUtil.toList(deviceTrigger));
// 调用
timerHandler.registerTimerTriggers(sceneRule);
// 验证 - 不应该调用调度器
verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any());
}
@Test
public void testRegisterTimerTriggers_emptyCronExpression() throws SchedulerException {
// 准备参数 - CRON 表达式为空
Long sceneRuleId = 2L;
IotSceneRuleDO sceneRule = new IotSceneRuleDO();
sceneRule.setId(sceneRuleId);
sceneRule.setStatus(0); // 0 表示启用
// 创建定时触发器但没有 CRON 表达式
IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger();
timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
timerTrigger.setCronExpression(""); // 空的 CRON 表达式
sceneRule.setTriggers(ListUtil.toList(timerTrigger));
// 调用
timerHandler.registerTimerTriggers(sceneRule);
// 验证 - 不应该调用调度器
verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any());
}
@Test
public void testUnregisterTimerTriggers_success() throws SchedulerException {
// 准备参数
Long sceneRuleId = 3L;
// 调用
timerHandler.unregisterTimerTriggers(sceneRuleId);
// 验证
verify(schedulerManager, times(1)).deleteJob("iot_scene_rule_timer_" + sceneRuleId);
}
@Test
public void testPauseTimerTriggers_success() throws SchedulerException {
// 准备参数
Long sceneRuleId = 4L;
// 调用
timerHandler.pauseTimerTriggers(sceneRuleId);
// 验证
verify(schedulerManager, times(1)).pauseJob("iot_scene_rule_timer_" + sceneRuleId);
}
}

View File

@@ -0,0 +1,141 @@
package cn.iocoder.yudao.module.iot.core.util;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* {@link IotDeviceMessageUtils} 的单元测试
*
* @author HUIHUI
*/
public class IotDeviceMessageUtilsTest {
@Test
public void testExtractPropertyValue_directValue() {
// 测试直接值(非 Map
IotDeviceMessage message = new IotDeviceMessage();
message.setParams(25.5);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_directIdentifier() {
// 测试直接通过标识符获取
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> params = new HashMap<>();
params.put("temperature", 25.5);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_propertiesStructure() {
// 测试 properties 结构
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> properties = new HashMap<>();
properties.put("temperature", 25.5);
properties.put("humidity", 60);
Map<String, Object> params = new HashMap<>();
params.put("properties", properties);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_dataStructure() {
// 测试 data 结构
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> data = new HashMap<>();
data.put("temperature", 25.5);
Map<String, Object> params = new HashMap<>();
params.put("data", data);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_valueField() {
// 测试 value 字段
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> params = new HashMap<>();
params.put("identifier", "temperature");
params.put("value", 25.5);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_singleValueMap() {
// 测试单值 Map包含 identifier 和一个值)
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> params = new HashMap<>();
params.put("identifier", "temperature");
params.put("actualValue", 25.5);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_notFound() {
// 测试未找到属性值
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> params = new HashMap<>();
params.put("humidity", 60);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertNull(result);
}
@Test
public void testExtractPropertyValue_nullParams() {
// 测试 params 为 null
IotDeviceMessage message = new IotDeviceMessage();
message.setParams(null);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertNull(result);
}
@Test
public void testExtractPropertyValue_priorityOrder() {
// 测试优先级顺序:直接标识符 > properties > data > value
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> properties = new HashMap<>();
properties.put("temperature", 20.0);
Map<String, Object> data = new HashMap<>();
data.put("temperature", 30.0);
Map<String, Object> params = new HashMap<>();
params.put("temperature", 25.5); // 最高优先级
params.put("properties", properties);
params.put("data", data);
params.put("value", 40.0);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result); // 应该返回直接标识符的值
}
}

View File

@@ -11,7 +11,6 @@ import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl;
import cn.binarywang.wx.miniapp.constant.WxMaConstants;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.ObjUtil;
@@ -102,8 +101,8 @@ public class SocialClientServiceImpl implements SocialClientService {
@Value("${yudao.wxa-subscribe-message.miniprogram-state:formal}")
public String miniprogramState;
// @Resource
@Autowired // TODO @芋艿:等 justauth1.4.1 发布,可以去掉
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@Autowired(required = false) // 由于 justauth.enable 配置项,可以关闭 AuthRequestFactory 的功能,所以这里只能不强制注入
private AuthRequestFactory authRequestFactory;
@Resource