Files
yidaima/RuoYi-Vue/sql/qing_templates.sql

5337 lines
164 KiB
MySQL
Raw Normal View History

2026-06-22 19:29:57 +08:00
-- Runnable three-tier code generation templates
-- Import this script after sql/db.sql.
-- Template ids: 9201 backend, 9202 admin_frontend, 9203 frontend
SET FOREIGN_KEY_CHECKS = 0;
DELETE FROM sys_project_template WHERE template_id IN (9201, 9202, 9203);
DELETE FROM sys_project_structure WHERE template_id IN (9201, 9202, 9203) OR node_id BETWEEN 920100 AND 920399;
DELETE FROM sys_template_file WHERE template_id IN (9201, 9202, 9203) OR template_file_id BETWEEN 920100 AND 920399;
DELETE FROM sys_template WHERE template_id IN (9201, 9202, 9203);
INSERT INTO sys_template (template_id, template_name, template_path, template_desc, template_type, template_status, create_by, create_time, remark) VALUES
(9201, 'Runnable Backend Template', '/runnable/backend', 'Spring Boot + MyBatis Plus runnable backend', 'backend', 0, 'admin', sysdate(), 'three-tier runnable template'),
(9202, 'Runnable Admin Frontend Template', '/runnable/admin_frontend', 'Vue2 + Element UI runnable admin frontend', 'admin_frontend', 0, 'admin', sysdate(), 'three-tier runnable template'),
(9203, 'Runnable Portal Frontend Template', '/runnable/frontend', 'Vue2 + Element UI runnable portal frontend', 'frontend', 0, 'admin', sysdate(), 'three-tier runnable template');
-- Backend template files
INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES
(920101, 9201, 'pom.xml.vm', NULL, 'pom.xml.vm', '<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.9</version>
<relativePath/>
</parent>
<groupId>${packageName}</groupId>
<artifactId>${businessName}-service</artifactId>
<version>1.0.0</version>
<name>${functionName}</name>
<description>${functionName} generated by qing template</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
', 'admin', sysdate()),
(920102, 9201, 'README.md.vm', NULL, 'README.md.vm', '# ${functionName}
qing Spring Boot + Vue2
## 后端启动
1. MySQL `vip`
2. `sql/${businessName}.sql`
3. `src/main/resources/application.yml`
4.
```bash
mvn spring-boot:run
```
The service starts at `http://localhost:8080`.
## 前端启动
```bash
cd vue
npm install
npm run dev
```
`/api` `http://localhost:8080`
', 'admin', sysdate()),
(920103, 9201, 'Application.java.vm', NULL, 'Application.java.vm', 'package ${packageName};
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("${packageName}.mapper")
@SpringBootApplication
public class ${ClassName}Application
{
public static void main(String[] args)
{
SpringApplication.run(${ClassName}Application.class, args);
}
}
', 'admin', sysdate()),
(920104, 9201, 'Result.java.vm', NULL, 'Result.java.vm', 'package ${packageName}.common;
import java.io.Serializable;
public class Result<T> implements Serializable
{
private static final long serialVersionUID = 1L;
private String code;
private String msg;
private T data;
public Result()
{
}
public Result(String code, String msg, T data)
{
this.code = code;
this.msg = msg;
this.data = data;
}
public static <T> Result<T> success()
{
return new Result<T>("200", "操作成功", null);
}
public static <T> Result<T> success(T data)
{
return new Result<T>("200", "操作成功", data);
}
public static <T> Result<T> error(String msg)
{
return new Result<T>("500", msg, null);
}
public String getCode()
{
return code;
}
public void setCode(String code)
{
this.code = code;
}
public String getMsg()
{
return msg;
}
public void setMsg(String msg)
{
this.msg = msg;
}
public T getData()
{
return data;
}
public void setData(T data)
{
this.data = data;
}
}
', 'admin', sysdate()),
(920105, 9201, 'MybatisPlusConfig.java.vm', NULL, 'MybatisPlusConfig.java.vm', 'package ${packageName}.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig
{
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor()
{
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
', 'admin', sysdate()),
(920106, 9201, 'CorsConfig.java.vm', NULL, 'CorsConfig.java.vm', 'package ${packageName}.config;
import ${packageName}.security.PortalAuthInterceptor;
import javax.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer
{
@Resource
private PortalAuthInterceptor portalAuthInterceptor;
@Override
public void addCorsMappings(CorsRegistry registry)
{
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(portalAuthInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/auth/login", "/auth/admin/login", "/business-blocks/**", "/files/**", "/error");
}
}
', 'admin', sysdate()),
(920107, 9201, 'FileController.java.vm', NULL, 'FileController.java.vm', 'package ${packageName}.controller;
import ${packageName}.common.Result;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/files")
public class FileController
{
private final Path uploadRoot = Paths.get("uploads");
@PostMapping("/upload")
public Result<Map<String, String>> upload(@RequestParam("file") MultipartFile file) throws IOException
{
Files.createDirectories(uploadRoot);
String originalName = StringUtils.cleanPath(file.getOriginalFilename() == null ? "file" : file.getOriginalFilename());
String suffix = "";
int dotIndex = originalName.lastIndexOf(''.'');
if (dotIndex >= 0)
{
suffix = originalName.substring(dotIndex);
}
String fileName = UUID.randomUUID().toString().replace("-", "") + suffix;
Files.copy(file.getInputStream(), uploadRoot.resolve(fileName), StandardCopyOption.REPLACE_EXISTING);
Map<String, String> data = new HashMap<String, String>();
data.put("name", originalName);
data.put("url", "/files/" + fileName);
return Result.success(data);
}
@GetMapping("/{fileName:.+}")
public ResponseEntity<Resource> getFile(@PathVariable String fileName) throws MalformedURLException
{
Path filePath = uploadRoot.resolve(fileName).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists())
{
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\\"" + resource.getFilename() + "\\"")
.body(resource);
}
}
', 'admin', sysdate()),
(920108, 9201, 'application.yml.vm', NULL, 'application.yml.vm', '#set($d = ''$'')
server:
port: ${d}{SERVER_PORT:8080}
spring:
application:
name: ${businessName}-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${d}{DB_URL:jdbc:mysql://localhost:3306/vip?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8}
username: ${d}{DB_USERNAME:root}
password: ${d}{DB_PASSWORD:123456}
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
', 'admin', sysdate()),
(920109, 9201, 'entity.java.vm', NULL, 'entity.java.vm', 'package ${packageName}.entity;
#foreach ($import in $importList)
import ${import};
#end
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import cn.hutool.core.annotation.Alias;
/**
* ${functionName} ${tableName}
*
* @author ${author}
* @date ${datetime}
*/
#if($table.crud || $table.sub)
#set($Entity="BaseEntity")
#elseif($table.tree)
#set($Entity="TreeEntity")
#end
@Getter
@Setter
@Accessors(chain = true)
@ApiModel(value = "${ClassName}对象", description = "${functionName}")
@TableName("${tableName}")
public class ${ClassName}
{
private static final long serialVersionUID = 1L;
#foreach ($column in $columns)
/** $column.columnComment */
#if($column.list)
#set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
#if($column.javaType == ''Date'')
@JsonFormat(pattern = "yyyy-MM-dd")
#else
#end
#end
@ApiModelProperty("${column.columnComment}")
@Alias("${column.columnComment}")
#if ($column.isPk == "1")
@TableId(value = "${column.columnName}", type = IdType.AUTO)
#else
@TableField("${column.columnName}")
#end
private $column.javaType $column.javaField;
#end
#if($table.sub)
/** $table.subTable.functionName信息 */
@ApiModelProperty("${table.subTable.functionName}信息")
private List<${subClassName}> ${subclassName}List;
#end
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
#foreach ($column in $columns)
#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]"))
#set($AttrName=$column.javaField)
#else
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#end
.append("${column.javaField}", get${AttrName}())
#end
#if($table.sub)
.append("${subclassName}List", get${subClassName}List())
#end
.toString();
}
}
', 'admin', sysdate()),
(920110, 9201, 'mapper.java.vm', NULL, 'mapper.java.vm', 'package ${packageName}.mapper;
import ${packageName}.entity.${ClassName};
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* ${functionName}Mapper接口
*
* @author ${author}
* @date ${datetime}
*/
public interface ${ClassName}Mapper extends BaseMapper<${ClassName}> {
}
', 'admin', sysdate()),
(920111, 9201, 'service.java.vm', NULL, 'service.java.vm', 'package ${packageName}.service;
import ${packageName}.entity.${ClassName};
import com.baomidou.mybatisplus.extension.service.IService;
#if($hasTableBusinessActions)
import java.util.Map;
#end
/**
* ${functionName}Service接口
*
* @author ${author}
* @date ${datetime}
*/
public interface I${ClassName}Service extends IService<${ClassName}> {
#if($hasTableBusinessActions)
#foreach($action in $tableBusinessActions)
int ${action.code}(Map<String, Object> params);
#end
#end
}
', 'admin', sysdate()),
(920112, 9201, 'serviceImpl.java.vm', NULL, 'serviceImpl.java.vm', 'package ${packageName}.service.impl;
import ${packageName}.entity.${ClassName};
import ${packageName}.mapper.${ClassName}Mapper;
import ${packageName}.service.I${ClassName}Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
#if($hasTableBusinessActions)
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;
#end
import org.springframework.stereotype.Service;
/**
* ${functionName}Service业务层处理
*
* @author ${author}
* @date ${datetime}
*/
@Service
public class ${ClassName}ServiceImpl extends ServiceImpl<${ClassName}Mapper, ${ClassName}> implements I${ClassName}Service {
#if($hasTableBusinessActions)
// STRICT_TARGET_TABLE_CONDITIONS
@Resource
private JdbcTemplate jdbcTemplate;
#foreach($action in $tableBusinessActions)
@Override
@Transactional(rollbackFor = Exception.class)
public int ${action.code}(Map<String, Object> params)
{
if (params == null)
{
throw new IllegalArgumentException("业务动作参数不能为空");
}
#if($action.requestFields)
#foreach($field in $action.requestFields)
requireBusinessParam(params, "${field}");
#end
#end
#if($action.ruleChecks)
#foreach($rule in $action.ruleChecks)
#set($conditionFields = $rule.conditionFields)
#if($rule.type == "EXISTS")
assertExists(params, "${rule.targetTable}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
#elseif($rule.type == "NOT_EXISTS")
assertNotExists(params, "${rule.targetTable}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
#elseif($rule.type == "FIELD_EQUALS")
assertFieldEquals(params, "${rule.targetTable}", "${rule.targetField}", "${rule.expectedValue}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
#elseif($rule.type == "FIELD_NOT_EQUALS")
assertFieldNotEquals(params, "${rule.targetTable}", "${rule.targetField}", "${rule.expectedValue}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
#elseif($rule.type == "FIELD_IN")
assertFieldIn(params, "${rule.targetTable}", "${rule.targetField}", new String[] {#foreach($value in $rule.expectedValues)"${value}"#if($foreach.hasNext), #end#end}, new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
#elseif($rule.type == "NUMBER_GTE")
assertNumberGte(params, "${rule.targetTable}", "${rule.targetField}", "${rule.compareValue}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
#end
#end
#end
int rows = 0;
#if($action.effects)
#foreach($effect in $action.effects)
#if($effect.type == "UPDATE_FIELD")
#set($conditionFields = $effect.conditionFields)
rows += executeUpdateFieldEffect(params, "${effect.targetTable}", "${effect.targetField}", "${effect.value}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end});
#elseif($effect.type == "INSERT_ROW")
#set($effectIndex = $foreach.index)
Map<String, Object> insertValues${effectIndex} = new java.util.LinkedHashMap<String, Object>();
#foreach($entry in $effect.values.entrySet())
insertValues${effectIndex}.put("${entry.key}", resolveBusinessValue(params, "${entry.value}"));
#end
rows += insertRow("${effect.targetTable}", insertValues${effectIndex});
#elseif($effect.type == "UPDATE_FIELDS")
#set($conditionFields = $effect.conditionFields)
#set($effectIndex = $foreach.index)
Map<String, Object> updateValues${effectIndex} = new java.util.LinkedHashMap<String, Object>();
#foreach($entry in $effect.values.entrySet())
updateValues${effectIndex}.put("${entry.key}", resolveBusinessValue(params, "${entry.value}"));
#end
rows += updateFields(params, "${effect.targetTable}", updateValues${effectIndex}, new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end});
#elseif($effect.type == "SET_STATUS")
#set($conditionFields = $effect.conditionFields)
rows += setStatus(params, "${effect.targetTable}", "${effect.targetField}", resolveBusinessValue(params, "${effect.value}"), new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end});
#elseif($effect.type == "INCREASE_NUMBER")
#set($conditionFields = $effect.conditionFields)
rows += increaseNumberField(params, "${effect.targetTable}", "${effect.targetField}", "${effect.amount}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end});
#elseif($effect.type == "DECREASE_NUMBER")
#set($conditionFields = $effect.conditionFields)
rows += decreaseNumberField(params, "${effect.targetTable}", "${effect.targetField}", "${effect.amount}", "${effect.minValue}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${effect.message}");
#end
#end
#end
return rows;
}
#end
private void requireBusinessParam(Map<String, Object> params, String field)
{
if (!params.containsKey(field) || params.get(field) == null || String.valueOf(params.get(field)).trim().isEmpty())
{
throw new IllegalArgumentException("缺少业务动作参数:" + field);
}
}
private void assertExists(Map<String, Object> params, String tableName, String[] conditionFields, String message)
{
if (countRows(params, tableName, conditionFields) <= 0)
{
throw new IllegalArgumentException(defaultMessage(message, "业务记录不存在"));
}
}
private void assertNotExists(Map<String, Object> params, String tableName, String[] conditionFields, String message)
{
if (countRows(params, tableName, conditionFields) > 0)
{
throw new IllegalArgumentException(defaultMessage(message, "业务记录已存在"));
}
}
private void assertFieldEquals(Map<String, Object> params, String tableName, String targetField,
String expectedValue, String[] conditionFields, String message)
{
Object value = querySingleField(params, tableName, targetField, conditionFields);
if (value == null || !String.valueOf(resolveBusinessValue(params, expectedValue)).equals(String.valueOf(value)))
{
throw new IllegalArgumentException(defaultMessage(message, "业务规则校验失败"));
}
}
private void assertFieldNotEquals(Map<String, Object> params, String tableName, String targetField,
String expectedValue, String[] conditionFields, String message)
{
Object value = querySingleField(params, tableName, targetField, conditionFields);
if (value != null && String.valueOf(resolveBusinessValue(params, expectedValue)).equals(String.valueOf(value)))
{
throw new IllegalArgumentException(defaultMessage(message, "业务规则校验失败"));
}
}
private void assertFieldIn(Map<String, Object> params, String tableName, String targetField,
String[] expectedValues, String[] conditionFields, String message)
{
Object value = querySingleField(params, tableName, targetField, conditionFields);
String current = value == null ? "" : String.valueOf(value);
for (String expectedValue : expectedValues)
{
if (String.valueOf(resolveBusinessValue(params, expectedValue)).equals(current))
{
return;
}
}
throw new IllegalArgumentException(defaultMessage(message, "业务规则校验失败"));
}
private void assertNumberGte(Map<String, Object> params, String tableName, String targetField,
String compareValue, String[] conditionFields, String message)
{
Object value = querySingleField(params, tableName, targetField, conditionFields);
BigDecimal current = toBigDecimal(value, "业务数值无效");
BigDecimal expected = resolveBusinessNumber(params, compareValue);
if (current.compareTo(expected) < 0)
{
throw new IllegalArgumentException(defaultMessage(message, "业务数值校验失败"));
}
}
private int insertRow(String tableName, Map<String, Object> values)
{
String safeTableName = requireSafeIdentifier(tableName, "业务动作目标表无效");
if (values == null || values.isEmpty())
{
throw new IllegalArgumentException("业务新增字段不能为空");
}
StringBuilder fields = new StringBuilder();
StringBuilder placeholders = new StringBuilder();
List<Object> args = new ArrayList<Object>();
for (Map.Entry<String, Object> entry : values.entrySet())
{
if (fields.length() > 0)
{
fields.append(", ");
placeholders.append(", ");
}
fields.append(requireSafeIdentifier(entry.getKey(), "业务新增字段无效"));
placeholders.append("?");
args.add(entry.getValue());
}
return jdbcTemplate.update("insert into " + safeTableName + " (" + fields + ") values (" + placeholders + ")",
args.toArray());
}
private int updateFields(Map<String, Object> params, String tableName, Map<String, Object> values,
String[] conditionFields)
{
String safeTableName = requireSafeIdentifier(tableName, "业务动作目标表无效");
if (values == null || values.isEmpty())
{
throw new IllegalArgumentException("业务更新字段不能为空");
}
StringBuilder sets = new StringBuilder();
List<Object> args = new ArrayList<Object>();
for (Map.Entry<String, Object> entry : values.entrySet())
{
if (sets.length() > 0)
{
sets.append(", ");
}
sets.append(requireSafeIdentifier(entry.getKey(), "业务更新字段无效")).append(" = ?");
args.add(entry.getValue());
}
String whereClause = buildWhereClause(params, conditionFields);
appendConditionValues(args, params, conditionFields);
return jdbcTemplate.update("update " + safeTableName + " set " + sets + " where " + whereClause,
args.toArray());
}
private int setStatus(Map<String, Object> params, String tableName, String targetField, Object value,
String[] conditionFields)
{
Map<String, Object> values = new java.util.LinkedHashMap<String, Object>();
values.put(targetField, value);
return updateFields(params, tableName, values, conditionFields);
}
private int increaseNumberField(Map<String, Object> params, String tableName, String targetField,
String amountExpression, String[] conditionFields)
{
String safeTableName = requireSafeIdentifier(tableName, "业务动作目标表无效");
String safeTargetField = requireSafeIdentifier(targetField, "业务动作目标字段无效");
BigDecimal amount = resolveBusinessNumber(params, amountExpression);
String whereClause = buildWhereClause(params, conditionFields);
List<Object> args = new ArrayList<Object>();
args.add(amount);
appendConditionValues(args, params, conditionFields);
return jdbcTemplate.update("update " + safeTableName + " set " + safeTargetField + " = "
+ safeTargetField + " + ? where " + whereClause, args.toArray());
}
private int decreaseNumberField(Map<String, Object> params, String tableName, String targetField,
String amountExpression, String minValueExpression,
String[] conditionFields, String message)
{
String safeTableName = requireSafeIdentifier(tableName, "业务动作目标表无效");
String safeTargetField = requireSafeIdentifier(targetField, "业务动作目标字段无效");
BigDecimal amount = resolveBusinessNumber(params, amountExpression);
BigDecimal minValue = resolveBusinessNumber(params,
minValueExpression == null || minValueExpression.length() == 0 ? "0" : minValueExpression);
String whereClause = buildWhereClause(params, conditionFields);
List<Object> args = new ArrayList<Object>();
args.add(amount);
appendConditionValues(args, params, conditionFields);
args.add(amount);
args.add(minValue);
int rows = jdbcTemplate.update("update " + safeTableName + " set " + safeTargetField + " = "
+ safeTargetField + " - ? where " + whereClause + " and " + safeTargetField + " - ? >= ?",
args.toArray());
if (rows == 0)
{
throw new IllegalArgumentException(defaultMessage(message, "业务数值扣减失败"));
}
return rows;
}
private int executeUpdateFieldEffect(Map<String, Object> params, String tableName, String targetField,
String valueExpression, String[] conditionFields)
{
String safeTableName = requireSafeIdentifier(tableName, "业务动作目标表无效");
String safeTargetField = requireSafeIdentifier(targetField, "业务动作目标字段无效");
String safeValueExpression = requireSafeExpression(valueExpression);
String whereClause = buildWhereClause(params, conditionFields);
return jdbcTemplate.update("update " + safeTableName + " set " + safeTargetField + " = "
+ safeValueExpression + " where " + whereClause, conditionValues(params, conditionFields));
}
private String buildWhereClause(Map<String, Object> params, String[] conditionFields)
{
if (conditionFields == null || conditionFields.length == 0)
{
throw new IllegalArgumentException("业务动作条件字段不能为空");
}
StringBuilder where = new StringBuilder();
for (String field : conditionFields)
{
String safeField = requireSafeIdentifier(field, "业务动作条件字段无效");
requireBusinessParam(params, safeField);
if (where.length() > 0)
{
where.append(" and ");
}
where.append(safeField).append(" = ?");
}
return where.toString();
}
private Object[] conditionValues(Map<String, Object> params, String[] conditionFields)
{
Object[] values = new Object[conditionFields.length];
for (int i = 0; i < conditionFields.length; i++)
{
values[i] = params.get(conditionFields[i]);
}
return values;
}
private int countRows(Map<String, Object> params, String tableName, String[] conditionFields)
{
String safeTableName = requireSafeIdentifier(tableName, "业务规则目标表无效");
String whereClause = buildWhereClause(params, conditionFields);
Integer count = jdbcTemplate.queryForObject("select count(1) from " + safeTableName + " where "
+ whereClause, conditionValues(params, conditionFields), Integer.class);
return count == null ? 0 : count.intValue();
}
private Object querySingleField(Map<String, Object> params, String tableName, String targetField,
String[] conditionFields)
{
String safeTableName = requireSafeIdentifier(tableName, "业务规则目标表无效");
String safeTargetField = requireSafeIdentifier(targetField, "业务规则目标字段无效");
String whereClause = buildWhereClause(params, conditionFields);
List<Map<String, Object>> rows = jdbcTemplate.queryForList("select " + safeTargetField + " from "
+ safeTableName + " where " + whereClause + " limit 1", conditionValues(params, conditionFields));
return rows.isEmpty() ? null : rows.get(0).get(safeTargetField);
}
private void appendConditionValues(List<Object> args, Map<String, Object> params, String[] conditionFields)
{
for (Object value : conditionValues(params, conditionFields))
{
args.add(value);
}
}
private Object resolveBusinessValue(Map<String, Object> params, String valueExpression)
{
if (valueExpression == null)
{
return null;
}
if ("#[[${now}]]#".equals(valueExpression))
{
return new Date();
}
if ("#[[${current_user.id}]]#".equals(valueExpression)
|| "#[[${current_user_id}]]#".equals(valueExpression))
{
requireBusinessParam(params, "__currentUserId");
return params.get("__currentUserId");
}
if (valueExpression.startsWith("#[[${param.]]#") && valueExpression.endsWith("}"))
{
String field = valueExpression.substring(8, valueExpression.length() - 1);
requireBusinessParam(params, field);
return params.get(field);
}
return valueExpression;
}
private BigDecimal resolveBusinessNumber(Map<String, Object> params, String valueExpression)
{
return toBigDecimal(resolveBusinessValue(params, valueExpression), "业务数值表达式无效");
}
private BigDecimal toBigDecimal(Object value, String message)
{
try
{
return new BigDecimal(String.valueOf(value));
}
catch (RuntimeException e)
{
throw new IllegalArgumentException(message);
}
}
private String defaultMessage(String message, String fallback)
{
return message == null || message.length() == 0 ? fallback : message;
}
private String requireSafeIdentifier(String value, String message)
{
if (value == null || !value.matches("[A-Za-z][A-Za-z0-9_]*"))
{
throw new IllegalArgumentException(message);
}
return value;
}
private String requireSafeExpression(String valueExpression)
{
if (valueExpression == null || !valueExpression.matches("[A-Za-z0-9_ +*/().-]{1,200}")
|| valueExpression.contains(";") || valueExpression.contains("--"))
{
throw new IllegalArgumentException("业务动作表达式无效");
}
return valueExpression;
}
#end
}
', 'admin', sysdate()),
(920113, 9201, 'controller.java.vm', NULL, 'controller.java.vm', 'package ${packageName}.controller;
import cn.hutool.poi.excel.ExcelReader;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import ${packageName}.common.Result;
import ${packageName}.entity.${ClassName};
#if($hasQingCurrentUserColumn || $hasTableBusinessActions)
import ${packageName}.entity.PortalUser;
import ${packageName}.security.PortalAuthTokenStore;
#end
import ${packageName}.service.I${ClassName}Service;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
#if($hasQingCurrentUserColumn || $hasTableBusinessActions)
import javax.servlet.http.HttpServletRequest;
#end
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* ${functionName}Controller
*
* @author ${author}
* @date ${datetime}
*/
@RestController
public class ${ClassName}Controller
{
@Resource
private I${ClassName}Service ${className}Service;
#if($hasQingCurrentUserColumn || $hasTableBusinessActions)
@Resource
private PortalAuthTokenStore tokenStore;
#end
#if($allowAdd == ''1'' || $allowEdit == ''1'')
@PostMapping("/${moduleName}/${businessName}")
public Result<Void> save(@RequestBody ${ClassName} ${className})
{
#if($allowAdd == ''1'' && $allowEdit == ''1'')
${className}Service.saveOrUpdate(${className});
#elseif($allowAdd == ''1'')
${className}Service.save(${className});
#else
${className}Service.updateById(${className});
#end
return Result.success();
}
#end
#if($allowRemove == ''1'')
@DeleteMapping("/${moduleName}/${businessName}/{${pkColumn.javaField}}")
public Result<Void> delete(@PathVariable ${pkColumn.javaType} ${pkColumn.javaField})
{
${className}Service.removeById(${pkColumn.javaField});
return Result.success();
}
@PostMapping("/${moduleName}/${businessName}/del/batch")
public Result<Void> deleteBatch(@RequestBody List<${pkColumn.javaType}> ${pkColumn.javaField}s)
{
${className}Service.removeByIds(${pkColumn.javaField}s);
return Result.success();
}
#end
@GetMapping("/${moduleName}/${businessName}")
public Result<List<${ClassName}>> findAll()
{
return Result.success(${className}Service.list());
}
@GetMapping("/${moduleName}/${businessName}/{${pkColumn.javaField}}")
public Result<${ClassName}> findOne(@PathVariable ${pkColumn.javaType} ${pkColumn.javaField})
{
return Result.success(${className}Service.getById(${pkColumn.javaField}));
}
@GetMapping("/${moduleName}/${businessName}/page")
public Result<Page<${ClassName}>> findPage(@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Map<String, Object> params#if($hasQingCurrentUserColumn),
HttpServletRequest request#end)
{
QueryWrapper<${ClassName}> queryWrapper = new QueryWrapper<${ClassName}>();
#if($hasQingCurrentUserColumn)
if (isCurrentUserOnly(params))
{
PortalUser currentUser = tokenStore.getPortalUser(resolveToken(request));
if (currentUser == null)
{
return Result.success(new Page<${ClassName}>(pageNum, pageSize));
}
queryWrapper.eq("${qingCurrentUserColumn.columnName}", currentUser.getUserId());
}
#end
#foreach ($column in $columns)
#if($column.isQuery == "1")
#set($queryType=$column.queryType)
#set($javaField=$column.javaField)
#set($columnName=$column.columnName)
#if($queryType == "EQ")
queryWrapper.eq(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
#elseif($queryType == "NE")
queryWrapper.ne(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
#elseif($queryType == "GT")
queryWrapper.gt(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
#elseif($queryType == "GTE")
queryWrapper.ge(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
#elseif($queryType == "LT")
queryWrapper.lt(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
#elseif($queryType == "LTE")
queryWrapper.le(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
#elseif($queryType == "LIKE")
queryWrapper.like(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
#else
queryWrapper.eq(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
#end
#end
#end
queryWrapper.orderByDesc("${pkColumn.columnName}");
return Result.success(${className}Service.page(new Page<${ClassName}>(pageNum, pageSize), queryWrapper));
}
@GetMapping("/${moduleName}/${businessName}/export")
public void export(HttpServletResponse response) throws Exception
{
List<${ClassName}> list = ${className}Service.list();
ExcelWriter writer = ExcelUtil.getWriter(true);
writer.write(list, true);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
String fileName = URLEncoder.encode("${functionName}信息表", "UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
ServletOutputStream out = response.getOutputStream();
writer.flush(out, true);
out.close();
writer.close();
}
#if($allowAdd == ''1'')
@PostMapping("/${moduleName}/${businessName}/import")
public Result<Void> imp(MultipartFile file) throws Exception
{
InputStream inputStream = file.getInputStream();
ExcelReader reader = ExcelUtil.getReader(inputStream);
List<${ClassName}> list = reader.readAll(${ClassName}.class);
${className}Service.saveBatch(list);
return Result.success();
}
#end
#if($hasTableBusinessActions)
#foreach($action in $tableBusinessActions)
#if($action.method == "GET")
@GetMapping("${action.path}")
public Result<Integer> ${action.code}(@RequestParam(required = false) Map<String, Object> params,
HttpServletRequest request)
#elseif($action.method == "PUT")
@org.springframework.web.bind.annotation.PutMapping("${action.path}")
public Result<Integer> ${action.code}(@RequestBody(required = false) Map<String, Object> params,
HttpServletRequest request)
#elseif($action.method == "DELETE")
@DeleteMapping("${action.path}")
public Result<Integer> ${action.code}(@RequestBody(required = false) Map<String, Object> params,
HttpServletRequest request)
#else
@PostMapping("${action.path}")
public Result<Integer> ${action.code}(@RequestBody(required = false) Map<String, Object> params,
HttpServletRequest request)
#end
{
try
{
Map<String, Object> actionParams = prepareBusinessActionParams(params, request, new String[] {#foreach($field in $action.currentUserFields)"${field}"#if($foreach.hasNext), #end#end});
return Result.success(${className}Service.${action.code}(actionParams));
}
catch (RuntimeException e)
{
return Result.error(e.getMessage());
}
}
#end
private Map<String, Object> prepareBusinessActionParams(Map<String, Object> params, HttpServletRequest request,
String[] currentUserFields)
{
Map<String, Object> actionParams = params == null
? new LinkedHashMap<String, Object>()
: new LinkedHashMap<String, Object>(params);
PortalUser currentUser = tokenStore.getPortalUser(resolveToken(request));
if (currentUser == null)
{
throw new IllegalArgumentException("请先登录");
}
actionParams.put("__currentUserId", currentUser.getUserId());
if (currentUserFields != null)
{
for (String field : currentUserFields)
{
if (StringUtils.isNotBlank(field))
{
if ("username".equals(field) || "user_name".equals(field))
{
actionParams.put(field, currentUser.getUsername());
}
else
{
actionParams.put(field, currentUser.getUserId());
}
}
}
}
return actionParams;
}
#end
private boolean hasValue(Object value)
{
return value != null && StringUtils.isNotBlank(String.valueOf(value));
}
#if($hasQingCurrentUserColumn)
private boolean isCurrentUserOnly(Map<String, Object> params)
{
return params != null && "true".equalsIgnoreCase(String.valueOf(params.get("__currentUserOnly")));
}
#end
#if($hasQingCurrentUserColumn || $hasTableBusinessActions)
private String resolveToken(HttpServletRequest request)
{
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer "))
{
return authorization.substring("Bearer ".length());
}
return request.getHeader("X-Portal-Token");
}
#end
}
', 'admin', sysdate()),
(920114, 9201, 'mapper.xml.vm', NULL, 'mapper.xml.vm', '<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${packageName}.mapper.${ClassName}Mapper">
<!-- 通用查询映射结果 -->
<resultMap type="${packageName}.entity.${ClassName}" id="BaseResultMap">
#foreach ($column in $columns)
#if ($column.isPk == "1")
<id column="${column.columnName}" property="${column.javaField}"/>
#else
<result column="${column.columnName}" property="${column.javaField}"/>
#end
#end
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
#foreach ($column in $columns)
${column.columnName}#if($foreach.count != $columns.size()),#end
#end
</sql>
</mapper>
', 'admin', sysdate()),
(920115, 9201, 'sql.vm', NULL, 'sql.vm', '-- ${functionName} table
-- Generated by qing template at ${datetime}
${table.createTableSql}
create table if not exists t_admin (
admin_id bigint not null auto_increment comment ''Admin ID'',
username varchar(64) not null comment ''Username'',
password_hash varchar(64) not null comment ''SHA-256 password hash'',
nickname varchar(64) default null comment ''Nickname'',
status char(1) default ''0'' comment ''Status, 0 normal 1 disabled'',
create_time datetime default current_timestamp comment ''Create time'',
primary key (admin_id),
unique key uk_t_admin_username (username)
) engine=innodb default charset=utf8mb4 comment=''Admin user table'';
insert into t_admin (username, password_hash, nickname, status)
select ''admin'',
''8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92'',
''Administrator'',
''0''
where not exists (select 1 from t_admin where username = ''admin'');
create table if not exists t_user (
user_id bigint not null auto_increment comment ''User ID'',
username varchar(64) not null comment ''Username'',
password_hash varchar(64) not null comment ''SHA-256 password hash'',
nickname varchar(64) default null comment ''Nickname'',
status char(1) default ''0'' comment ''Status, 0 normal 1 disabled'',
create_time datetime default current_timestamp comment ''Create time'',
primary key (user_id),
unique key uk_t_user_username (username)
) engine=innodb default charset=utf8mb4 comment=''Portal user table'';
insert into t_user (username, password_hash, nickname, status)
select ''user'',
''8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92'',
''User'',
''0''
where not exists (select 1 from t_user where username = ''user'');
', 'admin', sysdate()),
(920116, 9201, 'PortalUser.java.vm', NULL, 'PortalUser.java.vm', 'package ${packageName}.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
@Getter
@Setter
@Accessors(chain = true)
@TableName("t_user")
public class PortalUser
{
@TableId(value = "user_id", type = IdType.AUTO)
private Long userId;
private String username;
private String passwordHash;
private String nickname;
private String status;
}
', 'admin', sysdate()),
(920117, 9201, 'PortalUserMapper.java.vm', NULL, 'PortalUserMapper.java.vm', 'package ${packageName}.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import ${packageName}.entity.PortalUser;
public interface PortalUserMapper extends BaseMapper<PortalUser>
{
}
', 'admin', sysdate()),
(920118, 9201, 'PortalAuthTokenStore.java.vm', NULL, 'PortalAuthTokenStore.java.vm', 'package ${packageName}.security;
import ${packageName}.entity.AdminUser;
import ${packageName}.entity.PortalUser;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
@Component
public class PortalAuthTokenStore
{
private final Map<String, Object> sessions = new ConcurrentHashMap<String, Object>();
public String issue(Object user)
{
String token = UUID.randomUUID().toString().replace("-", "");
sessions.put(token, user);
return token;
}
public Object getUser(String token)
{
if (token == null || token.trim().isEmpty())
{
return null;
}
return sessions.get(token);
}
public PortalUser getPortalUser(String token)
{
Object user = getUser(token);
return user instanceof PortalUser ? (PortalUser) user : null;
}
public AdminUser getAdminUser(String token)
{
Object user = getUser(token);
return user instanceof AdminUser ? (AdminUser) user : null;
}
public boolean validate(String token)
{
return getUser(token) != null;
}
public void revoke(String token)
{
if (token != null)
{
sessions.remove(token);
}
}
}
', 'admin', sysdate()),
(920119, 9201, 'PortalAuthInterceptor.java.vm', NULL, 'PortalAuthInterceptor.java.vm', 'package ${packageName}.security;
import java.io.IOException;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class PortalAuthInterceptor implements HandlerInterceptor
{
@Resource
private PortalAuthTokenStore tokenStore;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws IOException
{
if ("OPTIONS".equalsIgnoreCase(request.getMethod()))
{
return true;
}
String token = resolveToken(request);
if (tokenStore.validate(token))
{
return true;
}
response.setStatus(401);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\\"code\\":\\"401\\",\\"msg\\":\\"\\",\\"data\\":null}");
return false;
}
private String resolveToken(HttpServletRequest request)
{
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer "))
{
return authorization.substring("Bearer ".length());
}
return request.getHeader("X-Portal-Token");
}
}
', 'admin', sysdate()),
(920120, 9201, 'PortalAuthController.java.vm', NULL, 'PortalAuthController.java.vm', 'package ${packageName}.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import ${packageName}.common.Result;
import ${packageName}.entity.PortalUser;
import ${packageName}.mapper.PortalUserMapper;
import ${packageName}.security.PortalAuthTokenStore;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/auth")
public class PortalAuthController
{
@Resource
private PortalUserMapper portalUserMapper;
@Resource
private PortalAuthTokenStore tokenStore;
@PostMapping("/login")
public Result<Map<String, Object>> login(@RequestBody LoginBody body)
{
if (body == null || isBlank(body.getUsername()) || isBlank(body.getPassword()))
{
return Result.error("请输入用户名和密码");
}
PortalUser user = portalUserMapper.selectOne(new QueryWrapper<PortalUser>()
.eq("username", body.getUsername())
.eq("status", "0")
.last("limit 1"));
if (user == null || !passwordHash(body.getPassword()).equalsIgnoreCase(user.getPasswordHash()))
{
return Result.error("账号或密码错误");
}
String token = tokenStore.issue(user);
user.setPasswordHash(null);
Map<String, Object> data = new HashMap<String, Object>();
data.put("token", token);
data.put("user", user);
return Result.success(data);
}
@GetMapping("/me")
public Result<PortalUser> me(HttpServletRequest request)
{
PortalUser user = tokenStore.getPortalUser(resolveToken(request));
if (user == null)
{
return Result.error("请先登录");
}
user.setPasswordHash(null);
return Result.success(user);
}
@PostMapping("/logout")
public Result<Void> logout(HttpServletRequest request)
{
tokenStore.revoke(resolveToken(request));
return Result.success();
}
private String resolveToken(HttpServletRequest request)
{
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer "))
{
return authorization.substring("Bearer ".length());
}
return request.getHeader("X-Portal-Token");
}
private String passwordHash(String password)
{
try
{
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
StringBuilder builder = new StringBuilder();
for (byte item : hash)
{
builder.append(String.format("%02x", item));
}
return builder.toString();
}
catch (Exception e)
{
throw new IllegalStateException("Password hash failed", e);
}
}
private boolean isBlank(String value)
{
return value == null || value.trim().isEmpty();
}
public static class LoginBody
{
private String username;
private String password;
public String getUsername()
{
return username;
}
public void setUsername(String username)
{
this.username = username;
}
public String getPassword()
{
return password;
}
public void setPassword(String password)
{
this.password = password;
}
}
}
', 'admin', sysdate()),
(920121, 9201, 'AdminUser.java.vm', NULL, 'AdminUser.java.vm', 'package ${packageName}.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
@Getter
@Setter
@Accessors(chain = true)
@TableName("t_admin")
public class AdminUser
{
@TableId(value = "admin_id", type = IdType.AUTO)
private Long adminId;
private String username;
private String passwordHash;
private String nickname;
private String status;
}
', 'admin', sysdate()),
(920122, 9201, 'AdminUserMapper.java.vm', NULL, 'AdminUserMapper.java.vm', 'package ${packageName}.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import ${packageName}.entity.AdminUser;
public interface AdminUserMapper extends BaseMapper<AdminUser>
{
}
', 'admin', sysdate()),
(920123, 9201, 'AdminAuthController.java.vm', NULL, 'AdminAuthController.java.vm', 'package ${packageName}.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import ${packageName}.common.Result;
import ${packageName}.entity.AdminUser;
import ${packageName}.mapper.AdminUserMapper;
import ${packageName}.security.PortalAuthTokenStore;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/auth/admin")
public class AdminAuthController
{
@Resource
private AdminUserMapper adminUserMapper;
@Resource
private PortalAuthTokenStore tokenStore;
@PostMapping("/login")
public Result<Map<String, Object>> login(@RequestBody LoginBody body)
{
if (body == null || isBlank(body.getUsername()) || isBlank(body.getPassword()))
{
return Result.error("请输入用户名和密码");
}
AdminUser user = adminUserMapper.selectOne(new QueryWrapper<AdminUser>()
.eq("username", body.getUsername())
.eq("status", "0")
.last("limit 1"));
if (user == null || !passwordHash(body.getPassword()).equalsIgnoreCase(user.getPasswordHash()))
{
return Result.error("账号或密码错误");
}
String token = tokenStore.issue(user);
user.setPasswordHash(null);
Map<String, Object> data = new HashMap<String, Object>();
data.put("token", token);
data.put("user", user);
return Result.success(data);
}
@GetMapping("/me")
public Result<AdminUser> me(HttpServletRequest request)
{
AdminUser user = tokenStore.getAdminUser(resolveToken(request));
if (user == null)
{
return Result.error("请先登录");
}
user.setPasswordHash(null);
return Result.success(user);
}
@PostMapping("/logout")
public Result<Void> logout(HttpServletRequest request)
{
tokenStore.revoke(resolveToken(request));
return Result.success();
}
private String resolveToken(HttpServletRequest request)
{
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer "))
{
return authorization.substring("Bearer ".length());
}
return request.getHeader("X-Admin-Token");
}
private String passwordHash(String password)
{
try
{
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
StringBuilder builder = new StringBuilder();
for (byte item : hash)
{
builder.append(String.format("%02x", item));
}
return builder.toString();
}
catch (Exception e)
{
throw new IllegalStateException("Password hash failed", e);
}
}
private boolean isBlank(String value)
{
return value == null || value.trim().isEmpty();
}
public static class LoginBody
{
private String username;
private String password;
public String getUsername()
{
return username;
}
public void setUsername(String username)
{
this.username = username;
}
public String getPassword()
{
return password;
}
public void setPassword(String password)
{
this.password = password;
}
}
}
', 'admin', sysdate());
-- Admin frontend template files
INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES
(920201, 9202, 'package.json.vm', NULL, 'package.json.vm', '{
"name": "${projectFileName}-admin",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vue-cli-service serve --host 0.0.0.0",
"serve": "vue-cli-service serve --host 0.0.0.0",
"build": "vue-cli-service build"
},
"dependencies": {
"axios": "^0.27.2",
"core-js": "^3.8.3",
"echarts": "5.4.0",
"element-ui": "^2.15.14",
"vue": "^2.6.14",
"vue-router": "^3.6.5"
},
"devDependencies": {
"@vue/cli-service": "^4.5.19",
"vue-template-compiler": "^2.6.14"
}
}', 'admin', sysdate()),
(920202, 9202, 'babel.config.js.vm', NULL, 'babel.config.js.vm', 'module.exports = {
presets: [
"@vue/cli-plugin-babel/preset"
]
}
', 'admin', sysdate()),
(920203, 9202, 'vue.config.js.vm', NULL, 'vue.config.js.vm', 'const port = process.env.PORT || 8081
const apiBaseUrl = process.env.VUE_APP_API_BASE_URL || "http://localhost:8080"
module.exports = {
transpileDependencies: [],
devServer: {
port,
proxy: {
"/api": {
target: apiBaseUrl,
changeOrigin: true,
pathRewrite: {
"^/api": ""
}
}
}
}
}
', 'admin', sysdate()),
(920204, 9202, 'index.html.vm', NULL, 'index.html.vm', '<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>${projectName} Admin</title>
</head>
<body>
<noscript> JavaScript 访</noscript>
<div id="app"></div>
</body>
</html>', 'admin', sysdate()),
(920205, 9202, 'admin-main.js.vm', NULL, 'admin-main.js.vm', 'import Vue from "vue"
import ElementUI from "element-ui"
import "element-ui/lib/theme-chalk/index.css"
import App from "./App.vue"
import router from "./router"
import request from "./utils/request"
import dict from "./utils/dict"
Vue.config.productionTip = false
Vue.use(ElementUI)
Vue.prototype.request = request
Vue.prototype._dict = dict
new Vue({
router,
render: h => h(App)
}).#[[$mount]]#("#app")', 'admin', sysdate()),
(920206, 9202, 'admin-App.vue.vm', NULL, 'admin-App.vue.vm', '<template>
<div id="app">
<router-view v-if="isLoginPage" />
<div v-else class="admin-shell">
<aside class="admin-sidebar">
<router-link class="brand" to="/">
<span class="brand-mark">ADM</span>
<span class="brand-text">${projectName}</span>
</router-link>
<nav class="sidebar-menu">
#if($hasAdminNavGroups)
#foreach($group in $adminNavGroups)
#if($group.hasChildren)
<div class="menu-group">
#if($group.clickable)
<router-link class="menu-title menu-title-link" :class=''{ active: isSidebarGroupActive(${group.activePathsLiteral}) }'' to="${group.path}" exact>
<span><i class="el-icon-menu"></i>${group.name}</span>
<i class="el-icon-arrow-up"></i>
</router-link>
#else
<div class="menu-title" :class=''{ active: isSidebarGroupActive(${group.activePathsLiteral}) }''>
<span><i class="el-icon-folder-opened"></i>${group.name}</span>
<i class="el-icon-arrow-up"></i>
</div>
#end
#foreach($child in $group.children)
<router-link class="sidebar-link is-child" to="${child.path}" exact>
<span>${child.name}</span>
</router-link>
#end
</div>
#else
<router-link class="sidebar-link" to="${group.path}" exact>
<i class="el-icon-menu"></i>
<span>${group.name}</span>
</router-link>
#end
#end
#else
<router-link class="sidebar-link" to="/empty" exact>
<i class="el-icon-warning-outline"></i>
<span></span>
</router-link>
#end
</nav>
</aside>
<section class="admin-main">
<header class="topbar">
<div class="breadcrumb">
<router-link to="/"></router-link>
<span>/</span>
<strong>{{ currentTitle }}</strong>
</div>
<el-dropdown trigger="click" @command="handleUserCommand">
<span class="user-entry">
<span class="avatar"></span>
<span>{{ userName }}</span>
<i class="el-icon-arrow-down"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="logout">退</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</header>
<main class="content-area">
<router-view />
</main>
</section>
</div>
</div>
</template>
<script>
export default {
name: "App",
computed: {
isLoginPage() {
return this.#[[$route]]#.path === "/login"
},
currentTitle() {
return this.#[[$route]]#.meta && this.#[[$route]]#.meta.title ? this.#[[$route]]#.meta.title : "系统首页"
},
userName() {
const storedUser = window.localStorage.getItem("admin-user")
if (storedUser) {
try {
const user = JSON.parse(storedUser)
return user.nickname || user.username || "管理员"
} catch (e) {
return "管理员"
}
}
return window.localStorage.getItem("adminUserName") || "管理员"
}
},
methods: {
isSidebarGroupActive(paths) {
return Array.isArray(paths) && paths.indexOf(this.#[[$route]]#.path) !== -1
},
handleUserCommand(command) {
if (command === "logout") {
this.request.post("/auth/admin/logout").finally(() => {
this.clearAdminSession()
this.#[[$router]]#.replace("/login")
})
}
},
clearAdminSession() {
window.localStorage.removeItem("admin-token")
window.localStorage.removeItem("admin-user")
}
}
}
</script>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
background: #f3f5fa;
color: #303133;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
a {
text-decoration: none;
}
.admin-shell {
min-height: 100vh;
display: flex;
background: #f3f5fa;
}
.admin-sidebar {
width: 208px;
min-height: 100vh;
flex: 0 0 208px;
overflow-y: auto;
background: #0f172a;
color: #d8e6f0;
}
.brand {
height: 62px;
padding: 0 16px;
display: flex;
align-items: center;
gap: 10px;
color: #fff;
background: #111827;
}
.brand-mark {
display: inline-grid;
width: 36px;
height: 36px;
place-items: center;
border-radius: 8px;
color: #ffffff;
background: #17633b;
font-size: 12px;
font-weight: 800;
letter-spacing: 0;
}
.brand-text {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 17px;
font-weight: 700;
}
.sidebar-menu {
padding: 10px 0 24px;
}
.sidebar-link,
.menu-title {
min-height: 44px;
padding: 0 18px;
display: flex;
align-items: center;
gap: 10px;
color: #d8e6f0;
font-size: 14px;
}
.menu-title {
justify-content: space-between;
background: #111f35;
border-left: 3px solid transparent;
}
.menu-title-link {
color: #d8e6f0;
}
.menu-title span {
display: flex;
align-items: center;
gap: 10px;
}
.menu-title.active,
.menu-title:hover,
.sidebar-link:hover {
color: #ffffff;
background: #1e2f4d;
}
.sidebar-link.is-child {
padding-left: 48px;
background: #0b1220;
}
.sidebar-link.router-link-exact-active,
.sidebar-link.router-link-active {
background: #2563eb;
color: #fff;
}
.admin-main {
min-width: 0;
flex: 1;
}
.topbar {
height: 56px;
padding: 0 18px;
display: flex;
align-items: center;
justify-content: space-between;
background: #ffffff;
border-bottom: 1px solid #e5e7eb;
color: #111827;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
}
.breadcrumb a,
.breadcrumb strong {
color: #111827;
}
.user-entry {
display: inline-flex;
align-items: center;
gap: 8px;
color: #334155;
cursor: pointer;
}
.avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: inline-grid;
place-items: center;
color: #1d4ed8;
background: #eff6ff;
font-weight: 700;
}
.content-area {
min-height: calc(100vh - 56px);
padding: 8px 10px 28px;
}
</style>', 'admin', sysdate()),
(920207, 9202, 'admin-router.js.vm', NULL, 'admin-router.js.vm', 'import Vue from "vue"
import Router from "vue-router"
import Login from "@/views/login/index.vue"
#if($hasAdminRouteItems)
#foreach($component in $adminRouteComponents)
import ${component.componentName} from "${component.componentPath}"
#end
#end
Vue.use(Router)
#if($hasAdminRouteItems)
const firstPath = "${defaultAdminPath}"
#else
const firstPath = "/empty"
const Empty = { template: "<div class=\"empty-page\">暂无可管理数据</div>" }
#end
const routes = [
{
path: "/login",
name: "Login",
component: Login,
meta: { title: "登录" }
},
{
path: "/",
redirect: firstPath
}#if($hasAdminRouteItems)#else,
{
path: "/empty",
name: "Empty",
component: Empty,
meta: { title: "系统首页" }
}#end
#foreach($item in $adminRouteItems),
{
path: "${item.path}",
name: "${item.code}",
component: ${item.componentName},
meta: { title: "${item.title}" }
}
#end
]
const router = new Router({
mode: "hash",
routes
})
router.beforeEach((to, from, next) => {
const hasToken = Boolean(window.localStorage.getItem("admin-token"))
if (to.path !== "/login" && !hasToken) {
next({ path: "/login", query: { redirect: to.fullPath } })
return
}
if (to.path === "/login" && hasToken) {
next(to.query.redirect || firstPath)
return
}
next()
})
export default router', 'admin', sysdate()),
(920211, 9202, 'admin-login.vue.vm', NULL, 'admin-login.vue.vm', '<template>
<div class="login-page">
<section class="login-visual">
<div class="login-brand">
<span class="brand-mark">
<i></i><i></i><i></i><i></i>
</span>
<span>${projectName}</span>
</div>
<h1>${projectName}</h1>
<p></p>
</section>
<el-form ref="loginForm" :model="form" :rules="rules" class="login-card" @keyup.enter.native="handleLogin">
<h2>${projectName}</h2>
<p></p>
<el-form-item prop="username">
<el-input v-model="form.username" prefix-icon="el-icon-user" placeholder="请输入账号" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="form.password" prefix-icon="el-icon-lock" placeholder="请输入密码" show-password />
</el-form-item>
<div class="login-options">
<el-checkbox v-model="form.remember"></el-checkbox>
</div>
<el-button type="primary" :loading="loading" @click="handleLogin"></el-button>
</el-form>
</div>
</template>
<script>
#if($hasTables)
#set($firstTable = $tables.get(0))
const defaultRedirect = "/${firstTable.moduleName}/${firstTable.businessName}"
#else
const defaultRedirect = "/empty"
#end
export default {
name: "AdminLogin",
data() {
return {
loading: false,
form: {
username: window.localStorage.getItem("adminUserName") || "admin",
password: "123456",
remember: true
},
rules: {
username: [{ required: true, message: "请输入账号", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }]
}
}
},
methods: {
handleLogin() {
this.#[[$refs]]#.loginForm.validate(valid => {
if (!valid) {
return
}
this.loading = true
this.request.post("/auth/admin/login", {
username: this.form.username,
password: this.form.password
}).then(res => {
if (!res || res.code !== "200" || !res.data || !res.data.token) {
throw new Error((res && res.msg) || "登录失败")
}
window.localStorage.setItem("admin-token", res.data.token)
window.localStorage.setItem("admin-user", JSON.stringify(res.data.user || {
username: this.form.username,
nickname: this.form.username
}))
if (this.form.remember) {
window.localStorage.setItem("adminUserName", this.form.username)
} else {
window.localStorage.removeItem("adminUserName")
}
this.#[[$message]]#.success("登录成功")
this.#[[$router]]#.replace(this.#[[$route]]#.query.redirect || defaultRedirect)
}).catch(error => {
this.#[[$message]]#.error(error.message || "登录失败")
}).finally(() => {
this.loading = false
})
})
}
}
}
</script>
<style scoped>
.login-page {
min-height: 100vh;
display: grid;
grid-template-columns: minmax(0, 1fr) 420px;
background: #f3f5fa;
}
.login-visual {
padding: 54px 64px;
display: flex;
flex-direction: column;
justify-content: center;
background: linear-gradient(135deg, #20a9b5 0%, #147a8f 48%, #001120 100%);
color: #fff;
}
.login-brand {
position: absolute;
top: 32px;
display: flex;
align-items: center;
gap: 12px;
font-size: 20px;
font-weight: 700;
}
.brand-mark {
width: 30px;
height: 30px;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 3px;
transform: rotate(45deg);
}
.brand-mark i {
display: block;
border-radius: 2px;
}
.brand-mark i:nth-child(1) {
background: #f4c542;
}
.brand-mark i:nth-child(2) {
background: #22b8cf;
}
.brand-mark i:nth-child(3) {
background: #2f80ed;
}
.brand-mark i:nth-child(4) {
background: #42c77b;
}
.login-visual h1 {
margin: 0;
font-size: 46px;
font-weight: 700;
letter-spacing: 0;
}
.login-visual p {
max-width: 520px;
margin: 16px 0 0;
color: rgba(255, 255, 255, .82);
font-size: 18px;
}
.login-card {
align-self: center;
margin: 0 48px 0 0;
padding: 36px 34px;
border-radius: 8px;
background: #fff;
box-shadow: 0 18px 42px rgba(0, 17, 32, .16);
}
.login-card h2 {
margin: 0;
color: #17233d;
font-size: 25px;
font-weight: 700;
letter-spacing: 0;
}
.login-card p {
margin: 8px 0 24px;
color: #8a95a6;
}
.login-options {
margin: -4px 0 18px;
}
.login-card .el-button {
width: 100%;
height: 42px;
border-color: #0f7dbd;
background: #0f7dbd;
}
@media (max-width: 900px) {
.login-page {
grid-template-columns: 1fr;
}
.login-visual {
min-height: 220px;
padding: 36px 28px;
}
.login-brand {
position: static;
margin-bottom: 48px;
}
.login-visual h1 {
font-size: 34px;
}
.login-card {
margin: 24px;
}
}
</style>
', 'admin', sysdate()),
(920208, 9202, 'request.js.vm', NULL, 'request.js.vm', 'import axios from "axios"
import { Message } from "element-ui"
const request = axios.create({
baseURL: "/api",
timeout: 30000
})
request.interceptors.request.use(config => {
const token = window.localStorage.getItem("admin-token")
if (token) {
config.headers.Authorization = "Bearer " + token
}
return config
})
request.interceptors.response.use(
response => {
const data = response.data
if (data && data.code === "401") {
clearAdminSession()
Message.error(data.msg || "请先登录")
return Promise.reject(new Error(data.msg || "Unauthorized"))
}
return data
},
error => {
if (error && error.response && error.response.status === 401) {
clearAdminSession()
Message.error("请先登录")
} else {
Message.error(error && error.message ? error.message : "请求失败")
}
return Promise.reject(error)
}
)
function clearAdminSession() {
window.localStorage.removeItem("admin-token")
window.localStorage.removeItem("admin-user")
if (window.location.hash.indexOf("#/login") !== 0) {
window.location.hash = "#/login"
}
}
export default request
', 'admin', sysdate()),
(920209, 9202, 'dict.js.vm', NULL, 'dict.js.vm', 'const emptyDict = []
export default {
getDictDataByType() {
return emptyDict
},
getDictLabel(type, value) {
return value === undefined || value === null || value === "" ? "-" : value
}
}
', 'admin', sysdate()),
(920210, 9202, 'admin-index.vue.vm', NULL, 'admin-index.vue.vm', '<template>
<div class="page-container">
<section class="query-panel">
<div class="toolbar query-toolbar">
#foreach ($column in $columns)
#if($column.isQuery == "1")
#set($javaField=$column.javaField)
#set($columnComment=$column.columnComment)
#if($column.htmlType == "input")
<el-input class="filter-item" placeholder="请输入${columnComment}" clearable v-model="queryParams.${javaField}" @keyup.enter.native="load" />
#elseif($column.htmlType == "select" || $column.htmlType == "radio")
<el-select class="filter-item" v-model="queryParams.${javaField}" placeholder="请选择${columnComment}" clearable>
<el-option
v-for="item in _dict.getDictDataByType(''${column.dictType}'')"
:key="item.value"
:label="item.name"
:value="item.value" />
</el-select>
#elseif($column.htmlType == "datetime")
<el-date-picker class="filter-item" v-model="queryParams.${javaField}" type="date" value-format="yyyy-MM-dd" placeholder="选择${columnComment}" />
#end
#end
#end
<el-button type="primary" @click="load"></el-button>
<el-button @click="reset"></el-button>
</div>
</section>
<section class="action-panel">
<div class="toolbar action-toolbar">
<el-button type="primary" icon="el-icon-plus" @click="handleAdd"></el-button>
<el-popconfirm
confirm-button-text="确定"
cancel-button-text="取消"
icon="el-icon-info"
icon-color="red"
title="确定批量删除选中的数据吗?"
@confirm="delBatch">
<el-button slot="reference" type="danger" icon="el-icon-delete"></el-button>
</el-popconfirm>
</div>
</section>
<section class="table-panel">
<el-table class="data-table" :data="${businessName}List" stripe @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="${pkColumn.javaField}" label="ID" width="90" />
#foreach ($column in $columns)
#if($column.isList == "1")
#set($javaField=$column.javaField)
#set($columnComment=$column.columnComment)
#if($column.htmlType == "datetime")
<el-table-column prop="${javaField}" label="${columnComment}" align="center" width="180" />
#elseif($column.htmlType == "imageUpload")
<el-table-column prop="${javaField}" label="${columnComment}" align="center" width="110">
<template slot-scope="scope">
<el-image
v-if="scope.row.${javaField}"
class="table-image"
:src="resolveResourceUrl(scope.row.${javaField})"
:preview-src-list="[resolveResourceUrl(scope.row.${javaField})]" />
<span v-else>-</span>
</template>
</el-table-column>
#elseif($column.htmlType == "select" || $column.htmlType == "radio")
<el-table-column prop="${javaField}" label="${columnComment}" align="center">
<template slot-scope="scope">
{{ _dict.getDictLabel(''${column.dictType}'', scope.row.${javaField}) }}
</template>
</el-table-column>
#else
<el-table-column prop="${javaField}" label="${columnComment}" align="center" />
#end
#end
#end
<el-table-column label="操作" width="190" align="center">
<template slot-scope="scope">
<el-button class="row-action edit-action" type="primary" icon="el-icon-edit" circle @click="handleEdit(scope.row)"></el-button>
<el-popconfirm
confirm-button-text="确定"
cancel-button-text="取消"
icon="el-icon-info"
icon-color="red"
title="确定删除这条数据吗?"
@confirm="del(scope.row.${pkColumn.javaField})">
<el-button slot="reference" class="row-action delete-action" type="danger" icon="el-icon-delete" circle></el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</section>
<section class="pagination-panel">
<el-pagination
class="pagination"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total" />
</section>
<el-dialog :title="form.${pkColumn.javaField} ? ''编辑${functionName}'' : ''新增${functionName}''" :visible.sync="dialogFormVisible" width="520px">
<el-form ref="form" :model="form" label-width="110px" size="small">
#foreach ($column in $columns)
#if($column.isEdit == "1")
#set($javaField=$column.javaField)
#set($columnComment=$column.columnComment)
<el-form-item label="${columnComment}" prop="${javaField}"#if($column.isRequired == "1") :rules="[{ required: true, message: ''${columnComment}不能为空'', trigger: #if($column.htmlType == "select")''change''#else''blur''#end }]"#end>
#if($column.htmlType == "input")
<el-input v-model="form.${javaField}" placeholder="请输入${columnComment}" />
#elseif($column.htmlType == "textarea")
<el-input v-model="form.${javaField}" type="textarea" placeholder="请输入${columnComment}" />
#elseif($column.htmlType == "select" || $column.htmlType == "radio")
<el-select v-model="form.${javaField}" placeholder="请选择${columnComment}" clearable>
<el-option
v-for="item in _dict.getDictDataByType(''${column.dictType}'')"
:key="item.value"
:label="item.name"
:value="item.value" />
</el-select>
#elseif($column.htmlType == "datetime")
<el-date-picker v-model="form.${javaField}" type="date" value-format="yyyy-MM-dd" placeholder="选择${columnComment}" />
#elseif($column.htmlType == "imageUpload")
<el-upload
class="avatar-uploader"
action="/api/files/upload"
:show-file-list="false"
:on-success="(res) => uploadSuccess(''${javaField}'', res)">
<img v-if="form.${javaField}" :src="resolveResourceUrl(form.${javaField})" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
#elseif($column.htmlType == "fileUpload")
<el-upload
action="/api/files/upload"
:on-success="(res) => uploadSuccess(''${javaField}'', res)">
<el-button size="small" type="primary"></el-button>
</el-upload>
<div v-if="form.${javaField}" class="file-link">{{ form.${javaField} }}</div>
#end
</el-form-item>
#end
#end
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false"></el-button>
<el-button type="primary" @click="save"></el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: "${BusinessName}",
data() {
return {
${businessName}List: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
#foreach ($column in $columns)
#if($column.isQuery == "1")
${column.javaField}: undefined,
#end
#end
},
form: {},
dialogFormVisible: false,
multipleSelection: []
}
},
created() {
this.load()
},
methods: {
load() {
this.request.get("/${moduleName}/${businessName}/page", {
params: this.queryParams
}).then(res => {
const page = res.data || {}
this.${businessName}List = page.records || []
this.total = page.total || 0
})
},
save() {
this.#[[$refs]]#.form.validate(valid => {
if (!valid) {
return
}
this.request.post("/${moduleName}/${businessName}", this.form).then(res => {
if (res.code === "200") {
this.#[[$message]]#.success("保存成功")
this.dialogFormVisible = false
this.load()
} else {
this.#[[$message]]#.error(res.msg || "保存失败")
}
})
})
},
handleAdd() {
this.form = {}
this.dialogFormVisible = true
this.#[[$nextTick]]#(() => this.#[[$refs]]#.form && this.#[[$refs]]#.form.clearValidate())
},
handleEdit(row) {
this.form = JSON.parse(JSON.stringify(row))
this.dialogFormVisible = true
this.#[[$nextTick]]#(() => this.#[[$refs]]#.form && this.#[[$refs]]#.form.clearValidate())
},
del(${pkColumn.javaField}) {
this.request.delete("/${moduleName}/${businessName}/" + ${pkColumn.javaField}).then(res => {
if (res.code === "200") {
this.#[[$message]]#.success("删除成功")
this.load()
} else {
this.#[[$message]]#.error(res.msg || "删除失败")
}
})
},
handleSelectionChange(val) {
this.multipleSelection = val
},
delBatch() {
const ids = this.multipleSelection.map(item => item.${pkColumn.javaField})
if (!ids.length) {
this.#[[$message]]#.warning("请先选择要删除的数据")
return
}
this.request.post("/${moduleName}/${businessName}/del/batch", ids).then(res => {
if (res.code === "200") {
this.#[[$message]]#.success("批量删除成功")
this.load()
} else {
this.#[[$message]]#.error(res.msg || "批量删除失败")
}
})
},
reset() {
this.queryParams = {
pageNum: 1,
pageSize: 10
}
this.load()
},
handleSizeChange(pageSize) {
this.queryParams.pageSize = pageSize
this.load()
},
handleCurrentChange(pageNum) {
this.queryParams.pageNum = pageNum
this.load()
},
resolveResourceUrl(url) {
if (!url) {
return ""
}
if (url.indexOf("http://") === 0 || url.indexOf("https://") === 0 || url.indexOf("//") === 0 || url.indexOf("data:") === 0 || url.indexOf("blob:") === 0) {
return url
}
if (url.indexOf("/api/") === 0) {
return url
}
if (url.indexOf("/files/") === 0) {
return "/api" + url
}
return url
},
uploadSuccess(field, res) {
if (res && res.code === "200" && res.data && res.data.url) {
this.#[[$set]]#(this.form, field, res.data.url)
} else {
this.#[[$message]]#.error("上传失败,请重试")
}
}
}
}
</script>
<style scoped>
.page-container {
min-height: calc(100vh - 72px);
}
.query-panel,
.action-panel,
.table-panel,
.pagination-panel {
margin-bottom: 8px;
border: 1px solid #edf0f5;
border-radius: 4px;
background: #fff;
box-shadow: 0 1px 8px rgba(26, 39, 56, .08);
}
.query-panel,
.action-panel {
padding: 12px 14px;
}
.table-panel {
padding: 12px 12px 0;
}
.pagination-panel {
padding: 10px 12px;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.query-toolbar .el-button--primary {
border-color: #cde8f0;
background: #eaf8fb;
color: #158a9a;
}
.query-toolbar .el-button--default {
border-color: #f6e6ba;
background: #fff8e6;
color: #c58600;
}
.action-toolbar .el-button--primary {
border-color: #9cd4ed;
background: #eefaff;
color: #0d80bd;
}
.action-toolbar .el-button--danger {
border-color: #f2c2c7;
background: #fff3f4;
color: #d64252;
}
.filter-item {
width: 220px;
}
.data-table {
width: 100%;
color: #606266;
font-size: 13px;
}
.data-table >>> th {
height: 48px;
background: #fff;
color: #7d8794;
font-weight: 600;
}
.data-table >>> td {
height: 58px;
border-color: #f0f2f5;
}
.data-table >>> .el-table__row--striped td {
background: #fafafa;
}
.pagination {
display: flex;
justify-content: flex-start;
}
.row-action {
width: 30px;
height: 30px;
padding: 0;
border: 0;
font-size: 14px;
}
.edit-action {
background: #0b7ec2;
}
.delete-action {
margin-left: 10px;
background: #e82f3d;
}
.table-image {
width: 52px;
height: 52px;
border-radius: 4px;
}
.avatar-uploader .avatar {
width: 96px;
height: 96px;
display: block;
object-fit: cover;
}
.avatar-uploader-icon {
width: 96px;
height: 96px;
line-height: 96px;
border: 1px dashed #d9d9d9;
border-radius: 4px;
text-align: center;
color: #8c939d;
}
.file-link {
margin-top: 6px;
color: #606266;
word-break: break-all;
}
</style>
', 'admin', sysdate());
-- Portal frontend template files
INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES
(920301, 9203, 'package.json.vm', NULL, 'package.json.vm', '{
"name": "${projectFileName}-web",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vue-cli-service serve --host 0.0.0.0",
"serve": "vue-cli-service serve --host 0.0.0.0",
"build": "vue-cli-service build"
},
"dependencies": {
"axios": "^0.27.2",
"core-js": "^3.8.3",
"echarts": "5.4.0",
"element-ui": "^2.15.14",
"vue": "^2.6.14",
"vue-router": "^3.6.5"
},
"devDependencies": {
"@vue/cli-service": "^4.5.19",
"vue-template-compiler": "^2.6.14"
}
}
', 'admin', sysdate()),
(920302, 9203, 'babel.config.js.vm', NULL, 'babel.config.js.vm', 'module.exports = {
presets: [
"@vue/cli-plugin-babel/preset"
]
}
', 'admin', sysdate()),
(920303, 9203, 'vue.config.js.vm', NULL, 'vue.config.js.vm', 'const port = process.env.PORT || 8082
const apiBaseUrl = process.env.VUE_APP_API_BASE_URL || "http://localhost:8080"
module.exports = {
transpileDependencies: [],
devServer: {
port,
proxy: {
"/api": {
target: apiBaseUrl,
changeOrigin: true,
pathRewrite: {
"^/api": ""
}
}
}
}
}
', 'admin', sysdate()),
(920304, 9203, 'index.html.vm', NULL, 'index.html.vm', '<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>${functionName}</title>
</head>
<body>
<noscript> JavaScript 访</noscript>
<div id="app"></div>
</body>
</html>
', 'admin', sysdate()),
(920305, 9203, 'frontend-main.js.vm', NULL, 'frontend-main.js.vm', 'import Vue from "vue"
import ElementUI from "element-ui"
import "element-ui/lib/theme-chalk/index.css"
import App from "./App.vue"
import router from "./router"
import request from "./api/request"
import dict from "./utils/dict"
import "./style.css"
Vue.config.productionTip = false
Vue.use(ElementUI)
Vue.prototype.request = request
Vue.prototype._dict = dict
new Vue({
router,
render: h => h(App)
}).#[[$mount]]#("#app")', 'admin', sysdate()),
(920306, 9203, 'frontend-App.vue.vm', NULL, 'frontend-App.vue.vm', '#set($portalProjectName = $projectName)
#if(!$portalProjectName || $portalProjectName == "")
#set($portalProjectName = $functionName)
#end
#if(!$portalProjectName || $portalProjectName == "")
#set($portalProjectName = "Portal")
#end
#set($portalHomePath = $defaultFrontendPath)
#if(!$portalHomePath || $portalHomePath == "")
#set($portalHomePath = "/${moduleName}/${businessName}")
#end
<template>
<div class="portal-shell">
<template v-if="!isLoginRoute">
<header class="top-nav">
<router-link class="brand" to="${portalHomePath}">
<span class="brand-logo" aria-hidden="true">APP</span>
<span>${portalProjectName}</span>
</router-link>
<nav class="portal-nav" aria-label="主导航">
#if($hasFrontendNavGroups)
#foreach($group in $frontendNavGroups)
<div class="portal-nav-group#if($group.hasChildren) has-children#end" :class=''{ active: isNavGroupActive(${group.activePathsLiteral}) }''>
#if($group.clickable)
<router-link class="portal-nav-parent" to="${group.path}" exact>${group.name}</router-link>
#else
<span class="portal-nav-parent portal-nav-directory">${group.name}</span>
#end
#if($group.hasChildren)
<button class="portal-nav-toggle" type="button" aria-label="展开子菜单"></button>
<div class="portal-nav-dropdown">
#foreach($child in $group.children)
<router-link class="portal-nav-child" to="${child.path}" exact>${child.name}</router-link>
#end
</div>
#end
</div>
#end
#else
<div class="portal-nav-group">
<router-link class="portal-nav-parent" to="${portalHomePath}" exact></router-link>
</div>
#end
</nav>
<div class="user-area">
<template v-if="currentUser">
<button class="user-chip" type="button">
<span class="avatar">{{ userInitial }}</span>
<span>{{ currentUserName }}</span>
</button>
<button class="logout-link" type="button" @click="logout">退</button>
</template>
<button v-else class="login-link" type="button" @click="goLogin"></button>
</div>
</header>
<section class="hero-banner" aria-label="项目横幅">
<div class="hero-pattern" aria-hidden="true"></div>
<h1>${portalProjectName}</h1>
</section>
</template>
<main :class="[''portal-main'', { ''portal-main-login'': isLoginRoute }]">
<router-view />
</main>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
currentUser: null
}
},
computed: {
isLoginRoute() {
return this.#[[$route]]#.path === "/login"
},
currentUserName() {
return this.currentUser && (this.currentUser.nickname || this.currentUser.username || "User")
},
userInitial() {
const name = this.currentUserName || "U"
return name.slice(0, 1).toUpperCase()
}
},
mounted() {
this.loadUser()
window.addEventListener("portal-user-change", this.loadUser)
},
beforeDestroy() {
window.removeEventListener("portal-user-change", this.loadUser)
},
methods: {
isNavGroupActive(paths) {
return Array.isArray(paths) && paths.indexOf(this.#[[$route]]#.path) !== -1
},
loadUser() {
const raw = localStorage.getItem("portal-user")
if (!raw) {
this.currentUser = null
return
}
try {
this.currentUser = JSON.parse(raw)
} catch (error) {
localStorage.removeItem("portal-user")
this.currentUser = null
}
},
goLogin() {
this.#[[$router]]#.push({
path: "/login",
query: { redirect: this.#[[$route]]#.fullPath }
})
},
logout() {
this.request.post("/auth/logout").catch(() => {})
localStorage.removeItem("portal-token")
localStorage.removeItem("portal-user")
window.dispatchEvent(new Event("portal-user-change"))
this.#[[$router]]#.push("/login")
}
}
}
</script>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
background: #f6f7fb;
color: #1f2937;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
button {
font: inherit;
}
.portal-shell {
min-height: 100vh;
background: #f6f7fb;
}
.top-nav {
position: sticky;
top: 0;
z-index: 20;
display: grid;
grid-template-columns: minmax(180px, auto) minmax(0, 1fr) auto;
align-items: center;
gap: 24px;
min-height: 68px;
padding: 0 32px;
color: #111827;
background: #ffffff;
border-bottom: 1px solid #e5e7eb;
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
}
.brand,
.portal-nav,
.user-area,
.user-chip {
display: inline-flex;
align-items: center;
}
.brand {
gap: 10px;
min-width: 0;
font-size: 18px;
font-weight: 700;
}
.brand span:last-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.brand-logo {
display: inline-grid;
width: 36px;
height: 36px;
flex: 0 0 36px;
place-items: center;
border-radius: 8px;
color: #ffffff;
background: #17633b;
font-size: 12px;
font-weight: 800;
letter-spacing: 0;
}
.portal-nav {
justify-content: center;
gap: 8px;
min-width: 0;
height: 100%;
overflow: visible;
}
.portal-nav-group {
position: relative;
display: inline-flex;
align-items: center;
flex: 0 0 auto;
min-height: 68px;
}
.portal-nav-parent,
.portal-nav-toggle,
.portal-nav-child {
display: inline-flex;
align-items: center;
border: 0;
background: transparent;
font: inherit;
cursor: pointer;
}
.portal-nav-parent {
min-height: 36px;
padding: 0 14px;
border-radius: 8px;
color: #1f2937;
font-size: 15px;
font-weight: 600;
white-space: nowrap;
}
.portal-nav-parent:hover,
.portal-nav-parent.router-link-active,
.portal-nav-group.active .portal-nav-parent {
color: #2563eb;
background: #eef5ff;
}
.portal-nav-directory {
cursor: default;
}
.portal-nav-toggle {
justify-content: center;
width: 28px;
height: 28px;
margin-left: -8px;
border-radius: 6px;
color: #64748b;
}
.portal-nav-group:hover .portal-nav-toggle,
.portal-nav-group:focus-within .portal-nav-toggle,
.portal-nav-group.active .portal-nav-toggle {
color: #2563eb;
background: #eef5ff;
}
.portal-nav-dropdown {
position: absolute;
top: calc(100% - 8px);
left: 0;
z-index: 30;
display: none;
min-width: 168px;
padding: 8px;
border: 1px solid #dbe4f0;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 16px 34px rgba(15, 23, 42, 0.14);
}
.portal-nav-group:hover .portal-nav-dropdown,
.portal-nav-group:focus-within .portal-nav-dropdown {
display: grid;
gap: 4px;
}
.portal-nav-child {
min-height: 34px;
padding: 0 10px;
border-radius: 6px;
color: #334155;
font-size: 14px;
white-space: nowrap;
}
.portal-nav-child:hover,
.portal-nav-child.router-link-active {
color: #2563eb;
background: #eef5ff;
}
.user-area {
justify-content: flex-end;
gap: 10px;
min-width: 130px;
}
.user-chip,
.login-link,
.logout-link {
border: 1px solid #dbe4f0;
color: #475569;
background: #ffffff;
cursor: pointer;
}
.user-chip {
gap: 8px;
padding: 0;
}
.avatar {
display: inline-grid;
width: 36px;
height: 36px;
place-items: center;
border: 1px solid #bfdbfe;
border-radius: 50%;
color: #1d4ed8;
background: #eff6ff;
font-weight: 700;
}
.logout-link,
.login-link {
min-height: 32px;
padding: 0 10px;
border-radius: 4px;
}
.logout-link:hover,
.login-link:hover {
color: #2563eb;
background: #f8fbff;
}
.hero-banner {
position: relative;
display: grid;
min-height: 260px;
place-items: center;
overflow: hidden;
background:
linear-gradient(132deg, rgba(24, 141, 149, 0.95), rgba(44, 120, 215, 0.88)),
#168d95;
}
.hero-pattern {
position: absolute;
inset: 0;
opacity: 0.58;
background:
linear-gradient(42deg, transparent 0 18%, rgba(255, 255, 255, 0.18) 18% 27%, transparent 27% 100%),
linear-gradient(132deg, transparent 0 48%, rgba(246, 186, 51, 0.32) 48% 55%, transparent 55% 100%);
}
.hero-banner h1 {
position: relative;
z-index: 1;
max-width: min(900px, calc(100% - 40px));
margin: 0;
color: #ffffff;
font-size: 46px;
font-weight: 600;
letter-spacing: 0;
line-height: 1.2;
text-align: center;
text-shadow: 0 12px 28px rgba(15, 23, 42, 0.22);
}
.portal-main {
width: min(1120px, calc(100% - 48px));
margin: 0 auto;
padding: 26px 0 44px;
}
.portal-main-login {
width: 100%;
min-height: 100vh;
margin: 0;
padding: 0;
}
@media (max-width: 900px) {
.top-nav {
grid-template-columns: 1fr auto;
gap: 10px;
height: auto;
min-height: 56px;
padding: 10px 16px;
}
.portal-nav {
grid-column: 1 / -1;
justify-content: flex-start;
order: 3;
gap: 6px;
overflow-x: auto;
overflow-y: visible;
}
.portal-nav-group {
min-height: 38px;
}
.portal-nav-parent {
min-height: 34px;
padding: 0 12px;
}
.portal-nav-dropdown {
position: fixed;
top: 104px;
left: 16px;
right: 16px;
}
.hero-banner {
min-height: 210px;
}
.hero-banner h1 {
font-size: 34px;
}
.portal-main {
width: min(100% - 28px, 1120px);
}
}
</style>
', 'admin', sysdate()),
(920307, 9203, 'style.css.vm', NULL, 'style.css.vm', 'body {
margin: 0;
background: #f6f7fb;
color: #1f2937;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
.el-button {
border-radius: 4px;
}', 'admin', sysdate()),
(920308, 9203, 'frontend-router.js.vm', NULL, 'frontend-router.js.vm', 'import Vue from "vue"
import Router from "vue-router"
import Login from "@/views/login/index.vue"
#set($portalDefaultPath = $defaultFrontendPath)
#if(!$portalDefaultPath || $portalDefaultPath == "")
#set($portalDefaultPath = "/${moduleName}/${businessName}")
#end
#if($hasFrontendRouteItems)
#foreach($component in $frontendRouteComponents)
import ${component.componentName} from "${component.componentPath}"
#end
#else
import ${BusinessName} from "@/views/${moduleName}/${businessName}/index.vue"
#end
Vue.use(Router)
const router = new Router({
mode: "hash",
routes: [
{
path: "/",
redirect: "${portalDefaultPath}"
},
{
path: "/login",
name: "Login",
component: Login
},
#if($hasFrontendRouteItems)
#foreach($item in $frontendRouteItems)
{
path: "${item.path}",
name: "${item.code}",
component: ${item.componentName},
meta: { requiresAuth: ${item.requiresLogin} }
}#if($foreach.hasNext),#end
#end
#else
{
path: "/${moduleName}/${businessName}",
name: "${BusinessName}",
component: ${BusinessName},
meta: { requiresAuth: true }
}
#end
]
})
router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta && record.meta.requiresAuth)
const hasToken = Boolean(localStorage.getItem("portal-token"))
if (requiresAuth && !hasToken) {
next({
path: "/login",
query: { redirect: to.fullPath }
})
return
}
next()
})
export default router
', 'admin', sysdate()),
(920309, 9203, 'frontend-request.js.vm', NULL, 'frontend-request.js.vm', 'import axios from "axios"
import { Message } from "element-ui"
const request = axios.create({
baseURL: "/api",
timeout: 30000
})
request.interceptors.request.use(config => {
const token = localStorage.getItem("portal-token")
if (token) {
config.headers.Authorization = "Bearer " + token
}
return config
})
request.interceptors.response.use(
response => {
const data = response.data
if (data && data.code === "401") {
clearSession()
Message.error(data.msg || "请先登录")
return Promise.reject(new Error(data.msg || "Unauthorized"))
}
return data
},
error => {
if (error && error.response && error.response.status === 401) {
clearSession()
Message.error("请先登录")
} else {
Message.error(error && error.message ? error.message : "请求失败")
}
return Promise.reject(error)
}
)
function clearSession() {
localStorage.removeItem("portal-token")
localStorage.removeItem("portal-user")
if (window.location.hash.indexOf("#/login") !== 0) {
window.location.hash = "#/login"
}
}
export default request
', 'admin', sysdate()),
(920310, 9203, 'frontend-api.js.vm', NULL, 'frontend-api.js.vm', 'import request from "./request"
export function list${ClassName}(query) {
return request({
url: "/${moduleName}/${businessName}/page",
method: "get",
params: query
})
}
export function get${ClassName}(${pkColumn.javaField}) {
return request({
url: "/${moduleName}/${businessName}/" + ${pkColumn.javaField},
method: "get"
})
}', 'admin', sysdate()),
(920311, 9203, 'frontend-index.vue.vm', NULL, 'frontend-index.vue.vm', '<template>
#set($designed = $hasCurrentFrontendPageDesign)
#set($queryColumns = $columns)
#set($tableColumns = $columns)
#set($formColumns = $columns)
#set($cardColumns = $columns)
#set($cardTitleColumn = false)
#set($cardImageColumn = false)
#set($showAdd = false)
#set($showBatchDelete = false)
#set($showView = false)
#set($showEdit = false)
#set($showDelete = false)
#set($isCardListLayout = false)
#set($isSplitListLayout = false)
#set($showQuery = true)
#set($showToolbar = true)
#set($showForm = true)
#set($renderForm = true)
#set($currentUserOnly = false)
#set($isDetailPage = $pageDesignIsDetail)
#set($detailColumns = $columns)
#set($toolbarBusinessActions = $pageDesignToolbarBusinessActions)
#set($rowBusinessActions = $pageDesignRowBusinessActions)
#set($allPageBusinessActions = $pageDesignBusinessActions)
#set($hasToolbarBusinessActions = $hasPageDesignToolbarBusinessActions)
#set($hasRowBusinessActions = $hasPageDesignRowBusinessActions)
#set($topBusinessBlocks = $pageDesignTopBusinessBlocks)
#set($bottomBusinessBlocks = $pageDesignBottomBusinessBlocks)
#set($embeddedBusinessBlocks = $pageDesignEmbeddedBusinessBlocks)
#if($allowAdd == ''1'')
#set($showAdd = true)
#end
#if($allowRemove == ''1'')
#set($showBatchDelete = true)
#set($showDelete = true)
#end
#if($allowEdit == ''1'')
#set($showEdit = true)
#end
#if($designed)
#set($queryColumns = $pageDesignQueryColumns)
#set($tableColumns = $pageDesignTableColumns)
#set($formColumns = $pageDesignFormColumns)
#set($cardColumns = $pageDesignCardColumns)
#set($cardTitleColumn = $pageDesignCardTitleColumn)
#set($cardImageColumn = $pageDesignCardImageColumn)
#set($showAdd = $pageDesignAllowAdd)
#set($showBatchDelete = $pageDesignAllowBatchDelete)
#set($showView = $pageDesignAllowView)
#set($showEdit = $pageDesignAllowEdit)
#set($showDelete = $pageDesignAllowDelete)
#set($isCardListLayout = $pageDesignIsCardList)
#set($isSplitListLayout = $pageDesignIsSplitList)
#set($showQuery = $pageDesignShowQuery)
#set($showToolbar = $pageDesignShowToolbar)
#set($showForm = $pageDesignShowForm)
#set($currentUserOnly = $pageDesignIsCurrentUserList)
#set($detailColumns = $pageDesignFormColumns)
#end
#if(!$showToolbar)
#set($showAdd = false)
#set($showBatchDelete = false)
#end
#if($isDetailPage)
#set($showQuery = false)
#set($showToolbar = false)
#set($showAdd = false)
#set($showBatchDelete = false)
#set($showView = false)
#set($showEdit = false)
#set($showDelete = false)
#set($showForm = false)
#set($renderForm = false)
#if($detailColumns.size() == 0)
#set($detailColumns = $columns)
#end
#end
#set($renderForm = $showForm)
#if($showAdd || $showEdit)
#set($renderForm = true)
#end
<section class="portal-page">
#if($hasPageDesignTopBusinessBlocks)
<section class="embedded-business-blocks top-business-blocks">
#foreach($block in $topBusinessBlocks)
<div class="embedded-business-block__item embedded-business-block__item--span-${block.span}">
<${block.componentName} />
</div>
#end
</section>
#end
<section class="content-board">
<div class="feature-card">
<div class="feature-illustration" aria-hidden="true">
<div class="device-face">
<span></span>
<span></span>
</div>
</div>
<div class="feature-copy">
<span></span>
<h2>${functionName}</h2>
<p></p>
</div>
</div>
<aside class="notice-panel">
<div class="notice-tabs">
<button
v-for="tab in noticeTabs"
:key="tab"
type="button"
:class="{ active: activeTab === tab }"
@click="activeTab = tab">
{{ tab }}
</button>
</div>
<ul class="notice-list">
<li v-for="notice in notices" :key="notice.title">
<strong>{{ notice.title }}</strong>
<span><i class="el-icon-time"></i>{{ notice.time }}</span>
</li>
</ul>
</aside>
</section>
<section class="data-panel#if($isSplitListLayout) split-data-panel#end">
<div class="panel-heading">
<div>
<span class="section-kicker"></span>
<h2>${functionName}</h2>
</div>
<div class="panel-actions">
#if($showToolbar)
#if($showAdd)
<el-button type="primary" icon="el-icon-plus" @click="handleAdd"></el-button>
#end
#if($showBatchDelete)
<el-popconfirm
confirm-button-text="确定"
cancel-button-text="取消"
icon="el-icon-info"
icon-color="red"
title="确定批量删除选中的数据吗?"
@confirm="delBatch">
<el-button slot="reference" type="danger" icon="el-icon-delete"></el-button>
</el-popconfirm>
#end
#foreach($action in $toolbarBusinessActions)
#set($buttonLabel = $action.buttonLabel)
#if(!$buttonLabel)#set($buttonLabel = $action.name)#end
#set($buttonType = $action.buttonType)
#if(!$buttonType)#set($buttonType = "primary")#end
#set($buttonIcon = $action.buttonIcon)
#if(!$buttonIcon)#set($buttonIcon = "el-icon-position")#end
<el-button type="${buttonType}" plain icon="${buttonIcon}" @click="handleBusinessAction(''${action.code}'')">${buttonLabel}</el-button>
#end
#end
</div>
</div>
#if($isDetailPage)
<div class="detail-view" v-loading="detailLoading">
<div v-if="detailLoaded && !detailRecord.${pkColumn.javaField}" class="empty-detail">
<i class="el-icon-document"></i>
<span></span>
</div>
<template v-else>
<dl class="detail-grid">
#foreach ($column in $detailColumns)
#set($javaField=$column.javaField)
#set($columnComment=$column.columnComment)
#set($remoteSelect=$qingRemoteSelectFields.contains($javaField))
<div class="detail-field#if($column.htmlType == "textarea") wide#end">
<dt>${columnComment}</dt>
<dd>
#if($column.htmlType == "imageUpload")
<el-image
v-if="detailRecord.${javaField}"
class="detail-image"
:src="resolveResourceUrl(detailRecord.${javaField})"
:preview-src-list="[resolveResourceUrl(detailRecord.${javaField})]" />
<span v-else>-</span>
#elseif($column.htmlType == "select" || $column.htmlType == "radio")
#if($remoteSelect)
{{ getOptionLabel(''${javaField}'', detailRecord.${javaField}) }}
#else
{{ _dict.getDictLabel(''${column.dictType}'', detailRecord.${javaField}) || ''-'' }}
#end
#else
{{ formatDetailValue(detailRecord.${javaField}) }}
#end
</dd>
</div>
#end
</dl>
#if($hasRowBusinessActions)
<div class="detail-actions">
#foreach($action in $rowBusinessActions)
#set($buttonLabel = $action.buttonLabel)
#if(!$buttonLabel)#set($buttonLabel = $action.name)#end
#set($buttonType = $action.buttonType)
#if(!$buttonType)#set($buttonType = "primary")#end
#set($buttonIcon = $action.buttonIcon)
#if(!$buttonIcon)#set($buttonIcon = "el-icon-position")#end
<el-button type="${buttonType}" icon="${buttonIcon}" @click="handleBusinessAction(''${action.code}'', detailRecord)">${buttonLabel}</el-button>
#end
</div>
#end
</template>
</div>
#else
#if($showQuery)
<div class="filter-bar">
#foreach ($column in $queryColumns)
#set($renderQueryField = false)
#if($designed)
#set($renderQueryField = true)
#elseif($column.isQuery == "1")
#set($renderQueryField = true)
#end
#if($renderQueryField)
#set($javaField=$column.javaField)
#set($columnComment=$column.columnComment)
#set($remoteSelect=$qingRemoteSelectFields.contains($javaField))
#if($column.htmlType == "input")
<el-input class="filter-item" placeholder="请输入${columnComment}" clearable v-model="queryParams.${javaField}" @keyup.enter.native="load" />
#elseif($column.htmlType == "select" || $column.htmlType == "radio")
<el-select class="filter-item" v-model="queryParams.${javaField}" placeholder="请选择${columnComment}" clearable>
#if($remoteSelect)
<el-option
v-for="item in getSelectOptions(''${javaField}'')"
:key="item.value"
:label="item.label"
:value="item.value" />
#else
<el-option
v-for="item in _dict.getDictDataByType(''${column.dictType}'')"
:key="item.value"
:label="item.name"
:value="item.value" />
#end
</el-select>
#elseif($column.htmlType == "datetime")
<el-date-picker class="filter-item" v-model="queryParams.${javaField}" type="date" value-format="yyyy-MM-dd" placeholder="选择${columnComment}" />
#end
#end
#end
<el-button type="primary" icon="el-icon-search" @click="load"></el-button>
<el-button icon="el-icon-refresh" @click="reset"></el-button>
</div>
#end
#if($isCardListLayout)
<div class="resource-card-grid">
<article v-for="item in ${businessName}List" :key="item.${pkColumn.javaField}" class="resource-card-item">
#if($showBatchDelete)
<el-checkbox class="resource-card-check" v-model="item.__checked" @change="handleCardSelectionChange" />
#end
<div class="resource-card-body">
#if($cardImageColumn)
<el-image
v-if="item.${cardImageColumn.javaField}"
class="resource-card-image"
:src="resolveResourceUrl(item.${cardImageColumn.javaField})"
:preview-src-list="[resolveResourceUrl(item.${cardImageColumn.javaField})]" />
#end
#if($cardTitleColumn)
<h3 class="resource-card-title">{{ item.${cardTitleColumn.javaField} || ''-'' }}</h3>
#end
#foreach ($column in $cardColumns)
#set($renderCardField = false)
#if($designed)
#set($renderCardField = true)
#elseif($column.isList == "1")
#set($renderCardField = true)
#end
#if($renderCardField)
#set($javaField=$column.javaField)
#set($columnComment=$column.columnComment)
#set($remoteSelect=$qingRemoteSelectFields.contains($javaField))
#if($column.htmlType == "imageUpload")
<el-image
v-if="item.${javaField}"
class="resource-card-image"
:src="resolveResourceUrl(item.${javaField})"
:preview-src-list="[resolveResourceUrl(item.${javaField})]" />
#elseif($column.htmlType == "select" || $column.htmlType == "radio")
<div class="resource-card-field">
<span>${columnComment}</span>
#if($remoteSelect)
<strong>{{ getOptionLabel(''${javaField}'', item.${javaField}) }}</strong>
#else
<strong>{{ _dict.getDictLabel(''${column.dictType}'', item.${javaField}) || ''-'' }}</strong>
#end
</div>
#else
<div class="resource-card-field">
<span>${columnComment}</span>
<strong>{{ item.${javaField} || ''-'' }}</strong>
</div>
#end
#end
#end
</div>
#if($showView || $showEdit || $showDelete || $hasRowBusinessActions)
<div class="resource-card-actions">
#if($showView)
<el-button type="text" icon="el-icon-view" @click="handleView(item)"></el-button>
#end
#if($showEdit)
<el-button type="text" icon="el-icon-edit" @click="handleEdit(item)"></el-button>
#end
#if($showDelete)
<el-popconfirm
confirm-button-text="确定"
cancel-button-text="取消"
icon="el-icon-info"
icon-color="red"
title="确定删除这条数据吗?"
@confirm="del(item.${pkColumn.javaField})">
<el-button slot="reference" type="text" icon="el-icon-delete" class="danger-text"></el-button>
</el-popconfirm>
#end
#foreach($action in $rowBusinessActions)
#set($buttonLabel = $action.buttonLabel)
#if(!$buttonLabel)#set($buttonLabel = $action.name)#end
#set($buttonType = $action.buttonType)
#if(!$buttonType)#set($buttonType = "text")#end
#set($buttonIcon = $action.buttonIcon)
#if(!$buttonIcon)#set($buttonIcon = "el-icon-position")#end
<el-button type="${buttonType}" icon="${buttonIcon}" @click="handleBusinessAction(''${action.code}'', item)">${buttonLabel}</el-button>
#end
</div>
#end
</article>
</div>
#else
<el-table :data="${businessName}List" border stripe class="resource-table" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
#if(!$designed)
<el-table-column prop="${pkColumn.javaField}" label="ID" width="90" />
#end
#foreach ($column in $tableColumns)
#set($renderTableField = false)
#if($designed)
#set($renderTableField = true)
#elseif($column.isList == "1")
#set($renderTableField = true)
#end
#if($renderTableField)
#set($javaField=$column.javaField)
#set($columnComment=$column.columnComment)
#set($remoteSelect=$qingRemoteSelectFields.contains($javaField))
#if($column.htmlType == "datetime")
<el-table-column prop="${javaField}" label="${columnComment}" align="center" width="180" />
#elseif($column.htmlType == "imageUpload")
<el-table-column prop="${javaField}" label="${columnComment}" align="center" width="110">
<template slot-scope="scope">
<el-image
v-if="scope.row.${javaField}"
class="table-image"
:src="resolveResourceUrl(scope.row.${javaField})"
:preview-src-list="[resolveResourceUrl(scope.row.${javaField})]" />
<span v-else>-</span>
</template>
</el-table-column>
#elseif($column.htmlType == "select" || $column.htmlType == "radio")
<el-table-column prop="${javaField}" label="${columnComment}" align="center">
<template slot-scope="scope">
#if($remoteSelect)
{{ getOptionLabel(''${javaField}'', scope.row.${javaField}) }}
#else
{{ _dict.getDictLabel(''${column.dictType}'', scope.row.${javaField}) }}
#end
</template>
</el-table-column>
#else
<el-table-column prop="${javaField}" label="${columnComment}" align="center" />
#end
#end
#end
#if($showView || $showEdit || $showDelete || $hasRowBusinessActions)
<el-table-column label="操作" width="230" align="center">
<template slot-scope="scope">
#if($showView)
<el-button type="text" icon="el-icon-view" @click="handleView(scope.row)"></el-button>
#end
#if($showEdit)
<el-button type="text" icon="el-icon-edit" @click="handleEdit(scope.row)"></el-button>
#end
#if($showDelete)
<el-popconfirm
confirm-button-text="确定"
cancel-button-text="取消"
icon="el-icon-info"
icon-color="red"
title="确定删除这条数据吗?"
@confirm="del(scope.row.${pkColumn.javaField})">
<el-button slot="reference" type="text" icon="el-icon-delete" class="danger-text"></el-button>
</el-popconfirm>
#end
#foreach($action in $rowBusinessActions)
#set($buttonLabel = $action.buttonLabel)
#if(!$buttonLabel)#set($buttonLabel = $action.name)#end
#set($buttonType = $action.buttonType)
#if(!$buttonType)#set($buttonType = "text")#end
#set($buttonIcon = $action.buttonIcon)
#if(!$buttonIcon)#set($buttonIcon = "el-icon-position")#end
<el-button type="${buttonType}" icon="${buttonIcon}" @click="handleBusinessAction(''${action.code}'', scope.row)">${buttonLabel}</el-button>
#end
</template>
</el-table-column>
#end
</el-table>
#end
<el-pagination
class="pagination"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total" />
#end
</section>
#if($hasPageDesignBottomBusinessBlocks)
<section class="embedded-business-blocks bottom-business-blocks">
#foreach($block in $bottomBusinessBlocks)
<div class="embedded-business-block__item embedded-business-block__item--span-${block.span}">
<${block.componentName} />
</div>
#end
</section>
#end
#if($renderForm)
<el-dialog :title="viewing ? ''查看${functionName}'' : (form.${pkColumn.javaField} ? ''编辑${functionName}'' : ''新增${functionName}'')" :visible.sync="dialogFormVisible" width="520px">
<el-form ref="form" :model="form" label-width="110px" size="small" :disabled="viewing">
#foreach ($column in $formColumns)
#set($renderFormField = false)
#if($designed)
#set($renderFormField = true)
#elseif($column.isEdit == "1")
#set($renderFormField = true)
#end
#if($renderFormField)
#set($javaField=$column.javaField)
#set($columnComment=$column.columnComment)
#set($remoteSelect=$qingRemoteSelectFields.contains($javaField))
#set($autoIncrementField=$column.isIncrement == "1")
<el-form-item#if($autoIncrementField) v-if="form.${javaField}"#end label="${columnComment}" prop="${javaField}"#if($column.isRequired == "1" && !$autoIncrementField) :rules="[{ required: true, message: ''${columnComment}不能为空'', trigger: #if($column.htmlType == "select")''change''#else''blur''#end }]"#end>
#if($column.htmlType == "input")
<el-input v-model="form.${javaField}"#if($autoIncrementField) disabled#end placeholder="请输入${columnComment}" />
#elseif($column.htmlType == "textarea")
<el-input v-model="form.${javaField}" type="textarea" placeholder="请输入${columnComment}" />
#elseif($column.htmlType == "select" || $column.htmlType == "radio")
<el-select v-model="form.${javaField}" placeholder="请选择${columnComment}" clearable>
#if($remoteSelect)
<el-option
v-for="item in getSelectOptions(''${javaField}'')"
:key="item.value"
:label="item.label"
:value="item.value" />
#else
<el-option
v-for="item in _dict.getDictDataByType(''${column.dictType}'')"
:key="item.value"
:label="item.name"
:value="item.value" />
#end
</el-select>
#elseif($column.htmlType == "datetime")
<el-date-picker v-model="form.${javaField}" type="date" value-format="yyyy-MM-dd" placeholder="选择${columnComment}" />
#elseif($column.htmlType == "imageUpload")
<el-upload
class="avatar-uploader"
action="/api/files/upload"
:show-file-list="false"
:on-success="(res) => uploadSuccess(''${javaField}'', res)">
<img v-if="form.${javaField}" :src="resolveResourceUrl(form.${javaField})" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
#elseif($column.htmlType == "fileUpload")
<el-upload
action="/api/files/upload"
:on-success="(res) => uploadSuccess(''${javaField}'', res)">
<el-button size="small" type="primary"></el-button>
</el-upload>
<div v-if="form.${javaField}" class="file-link">{{ form.${javaField} }}</div>
#end
</el-form-item>
#end
#end
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false"></el-button>
<el-button v-if="!viewing" type="primary" @click="save"></el-button>
</div>
</el-dialog>
#end
<el-dialog
:title="businessActionDialogTitle"
:visible.sync="businessActionDialogVisible"
width="520px"
append-to-body
:show-close="!businessActionSubmitting"
:close-on-click-modal="false"
:close-on-press-escape="!businessActionSubmitting"
@closed="resetBusinessActionDialog">
<p v-if="activeBusinessAction && activeBusinessAction.confirmMessage" class="business-action-hint">
{{ activeBusinessAction.confirmMessage }}
</p>
<el-form
ref="businessActionForm"
:model="businessActionForm"
label-width="110px"
size="small">
<el-form-item
v-for="field in businessActionInputFields"
:key="field.field"
:label="field.label"
:prop="field.field"
:rules="[{ required: true, message: field.label + ''不能为空'', trigger: field.htmlType === ''select'' ? ''change'' : ''blur'' }]">
<el-date-picker
v-if="field.htmlType === ''datetime''"
v-model="businessActionForm[field.field]"
type="date"
value-format="yyyy-MM-dd"
:placeholder="''请选择'' + field.label" />
<el-select
v-else-if="field.htmlType === ''select'' && field.dictType"
v-model="businessActionForm[field.field]"
clearable
:placeholder="''请选择'' + field.label">
<el-option
v-for="option in getBusinessActionFieldOptions(field)"
:key="option.value"
:label="option.label"
:value="option.value" />
</el-select>
<el-input
v-else-if="field.htmlType === ''textarea''"
v-model="businessActionForm[field.field]"
type="textarea"
:rows="3"
:placeholder="''请输入'' + field.label" />
<el-input
v-else
v-model="businessActionForm[field.field]"
:placeholder="''请输入'' + field.label" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button
:disabled="businessActionSubmitting"
@click="businessActionDialogVisible = false">
</el-button>
<el-button
type="primary"
:loading="businessActionSubmitting"
@click="submitBusinessAction">
{{ activeBusinessAction && activeBusinessAction.buttonLabel ? activeBusinessAction.buttonLabel : '''' }}
</el-button>
</div>
</el-dialog>
</section>
</template>
<script>
#foreach($block in $embeddedBusinessBlocks)
import ${block.componentName} from "${block.relativeImportPath}"
#end
export default {
name: "${BusinessName}",
#if($hasPageDesignEmbeddedBusinessBlocks)
components: {
#foreach($block in $embeddedBusinessBlocks)
${block.componentName}#if($foreach.hasNext),#end
#end
},
#end
data() {
return {
${businessName}List: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
#if($currentUserOnly)
__currentUserOnly: true,
#end
#foreach ($column in $queryColumns)
#set($renderQueryParam = false)
#if(!$showQuery)
#set($renderQueryParam = false)
#elseif($designed)
#set($renderQueryParam = true)
#elseif($column.isQuery == "1")
#set($renderQueryParam = true)
#end
#if($renderQueryParam)
${column.javaField}: undefined,
#end
#end
},
#if($isDetailPage)
detailRecord: {},
detailLoading: false,
detailLoaded: false,
#end
form: {},
viewing: false,
dialogFormVisible: false,
businessActionDialogVisible: false,
businessActionSubmitting: false,
activeBusinessAction: null,
businessActionBasePayload: {},
businessActionForm: {},
businessActionInputFields: [],
multipleSelection: [],
remoteOptionMap: {},
remoteSelectConfig: {
#foreach($remote in $qingRemoteSelects)
${remote.field}: {
path: "${remote.path}",
valueField: "${remote.valueField}",
labelField: "${remote.labelField}"
}#if($foreach.hasNext),#end
#end
},
activeTab: "最新公告",
noticeTabs: ["最新公告", "热门赛事", "热门攻略", "热门帖子"],
notices: [
{ title: "《${functionName}》资源内容更新与支持", time: "2025-06-24 16:50:40" },
{ title: "社区活动与资源维护窗口预告", time: "2025-06-24 16:48:30" },
{ title: "新内容发布和玩家反馈整理", time: "2025-06-24 10:30:24" },
{ title: "版本资料、攻略和周边内容同步", time: "2025-06-23 19:20:12" }
],
businessActionMap: {
#foreach($action in $allPageBusinessActions)
"${action.code}": {
name: "${action.name}",
buttonLabel: "${action.buttonLabel}",
method: "${action.method}",
path: "${action.path}",
successMessage: "${action.successMessage}",
refreshTarget: "${action.refreshTarget}",
confirmMessage: "${action.confirmMessage}",
requestFields: [#foreach($field in $action.requestFields)"${field}"#if($foreach.hasNext), #end#end],
currentUserFields: [#foreach($field in $action.currentUserFields)"${field}"#if($foreach.hasNext), #end#end],
inputFields: [
#foreach($field in $action.inputFields)
{
field: "${field.field}",
label: "${field.label}",
htmlType: "${field.htmlType}",
dictType: "${field.dictType}"
}#if($foreach.hasNext),#end
#end
]
}#if($foreach.count != $allPageBusinessActions.size()),#end
#end
},
routeActionMap: {
#foreach($routeAction in $pageDesignRouteActionEntries)
"${routeAction.key}": {
path: "${routeAction.path}",
paramMap: {#set($paramMap = $routeAction.paramMap)#foreach($param in $paramMap.entrySet())"${param.key}":"${param.value}"#if($foreach.count != $paramMap.size()),#end#end}
}#if($foreach.count != $pageDesignRouteActionEntries.size()),#end
#end
}
}
},
computed: {
businessActionDialogTitle() {
if (!this.activeBusinessAction) {
return "业务操作"
}
return this.activeBusinessAction.buttonLabel || this.activeBusinessAction.name || "业务操作"
}
},
created() {
this.loadRemoteOptions()
#if($isDetailPage)
this.loadDetail()
#else
this.load()
#end
},
methods: {
loadRemoteOptions() {
Object.keys(this.remoteSelectConfig).forEach(field => {
const config = this.remoteSelectConfig[field]
this.request.get(config.path, {
params: { pageNum: 1, pageSize: 999 }
}).then(res => {
const data = res.data || {}
const records = Array.isArray(data) ? data : (data.records || [])
const options = records.map(item => ({
value: item[config.valueField],
label: item[config.labelField]
}))
this.#[[$set]]#(this.remoteOptionMap, field, options)
})
})
},
getSelectOptions(field) {
return this.remoteOptionMap[field] || []
},
getOptionLabel(field, value) {
const option = this.getSelectOptions(field).find(item => String(item.value) === String(value))
if (option) {
return option.label
}
return value === undefined || value === null || value === "" ? "-" : value
},
#if(!$isDetailPage)
load() {
this.request.get("/${moduleName}/${businessName}/page", {
params: this.queryParams
}).then(res => {
const page = res.data || {}
this.${businessName}List = page.records || []
this.total = page.total || 0
})
},
#end
#if($isDetailPage)
resolveDetailId() {
const params = this.#[[$route]]# && this.#[[$route]]#.params ? this.#[[$route]]#.params : {}
return params.id || params.${pkColumn.javaField} || ""
},
loadDetail() {
const detailId = this.resolveDetailId()
if (!detailId) {
this.detailRecord = {}
this.form = {}
this.detailLoaded = true
return
}
this.detailLoading = true
this.request.get("/${moduleName}/${businessName}/" + detailId).then(res => {
this.detailRecord = res.data || {}
this.form = JSON.parse(JSON.stringify(this.detailRecord))
this.detailLoaded = true
}).finally(() => {
this.detailLoading = false
})
},
formatDetailValue(value) {
return value === undefined || value === null || value === "" ? "-" : value
},
#end
save() {
this.#[[$refs]]#.form.validate(valid => {
if (!valid) {
return
}
this.request.post("/${moduleName}/${businessName}", this.form).then(res => {
if (res.code === "200") {
this.#[[$message]]#.success("保存成功")
this.dialogFormVisible = false
this.load()
} else {
this.#[[$message]]#.error(res.msg || "保存失败")
}
})
})
},
handleAdd() {
if (this.handleConfiguredRoute("toolbar:create")) {
return
}
this.form = {}
this.viewing = false
this.dialogFormVisible = true
this.#[[$nextTick]]#(() => this.#[[$refs]]#.form && this.#[[$refs]]#.form.clearValidate())
},
handleView(row) {
if (this.handleConfiguredRoute("table:view", row)) {
return
}
if (this.handleConfiguredRoute("cardActions:view", row)) {
return
}
this.form = JSON.parse(JSON.stringify(row))
this.viewing = true
this.dialogFormVisible = true
},
handleEdit(row) {
if (this.handleConfiguredRoute("table:edit", row)) {
return
}
if (this.handleConfiguredRoute("cardActions:edit", row)) {
return
}
this.form = JSON.parse(JSON.stringify(row))
this.viewing = false
this.dialogFormVisible = true
this.#[[$nextTick]]#(() => this.#[[$refs]]#.form && this.#[[$refs]]#.form.clearValidate())
},
del(${pkColumn.javaField}) {
this.request.delete("/${moduleName}/${businessName}/" + ${pkColumn.javaField}).then(res => {
if (res.code === "200") {
this.#[[$message]]#.success("删除成功")
this.load()
} else {
this.#[[$message]]#.error(res.msg || "删除失败")
}
})
},
handleSelectionChange(val) {
this.multipleSelection = val
},
handleCardSelectionChange() {
this.multipleSelection = this.${businessName}List.filter(item => item.__checked)
},
handleConfiguredRoute(actionKey, row) {
const action = this.routeActionMap[actionKey]
if (!action || !action.path) {
return false
}
const path = this.buildConfiguredRoutePath(action.path, action.paramMap || {}, row)
if (!path) {
return false
}
this.#[[$router]]#.push(path)
return true
},
buildConfiguredRoutePath(routePath, paramMap, row) {
let path = routePath
Object.keys(paramMap || {}).forEach(param => {
const field = paramMap[param]
const value = row && row[field]
if (value !== undefined && value !== null && value !== "") {
path = path.replace(new RegExp(":" + param + "(?=/|$)", "g"), encodeURIComponent(value))
}
})
return this.hasUnresolvedRouteParam(path) ? "" : path
},
hasUnresolvedRouteParam(path) {
return String(path || "").split("/").some(segment => segment.indexOf(":") === 0)
},
delBatch() {
const ids = this.multipleSelection.map(item => item.${pkColumn.javaField})
if (!ids.length) {
this.#[[$message]]#.warning("请先选择要删除的数据")
return
}
this.request.post("/${moduleName}/${businessName}/del/batch", ids).then(res => {
if (res.code === "200") {
this.#[[$message]]#.success("批量删除成功")
this.load()
} else {
this.#[[$message]]#.error(res.msg || "批量删除失败")
}
})
},
reset() {
this.queryParams = {
pageNum: 1,
pageSize: 10#if($currentUserOnly),
__currentUserOnly: true#end
}
this.load()
},
handleBusinessAction(code, row) {
if (this.handleConfiguredRoute("business:" + code, row) || this.handleConfiguredRoute("toolbar:" + code, row) || this.handleConfiguredRoute("table:" + code, row)) {
return
}
const action = this.businessActionMap[code]
if (!action || !action.path) {
this.#[[$message]]#.warning("动作未配置")
return
}
const payload = this.buildBusinessActionPayload(action, row)
const missingFields = (action.inputFields || []).filter(field => !this.hasBusinessActionValue(payload[field.field]))
if (missingFields.length > 0) {
this.openBusinessActionDialog(action, payload, missingFields)
return
}
if (action.confirmMessage) {
this.#[[$confirm]]#(action.confirmMessage, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => this.executeBusinessAction(action, payload)).catch(() => {})
return
}
this.executeBusinessAction(action, payload).catch(() => {})
},
openBusinessActionDialog(action, payload, fields) {
const form = {}
fields.forEach(field => {
form[field.field] = this.hasBusinessActionValue(payload[field.field]) ? payload[field.field] : undefined
})
this.activeBusinessAction = action
this.businessActionBasePayload = Object.assign({}, payload)
this.businessActionInputFields = fields.slice()
this.businessActionForm = form
this.businessActionDialogVisible = true
this.#[[$nextTick]]#(() => {
const formRef = this.#[[$refs]]#.businessActionForm
if (formRef) {
formRef.clearValidate()
}
})
},
submitBusinessAction() {
const formRef = this.#[[$refs]]#.businessActionForm
if (!formRef || !this.activeBusinessAction) {
return
}
formRef.validate(valid => {
if (!valid) {
return
}
const action = this.activeBusinessAction
const payload = Object.assign({}, this.businessActionBasePayload, this.businessActionForm)
this.businessActionSubmitting = true
this.executeBusinessAction(action, payload)
.then(() => {
this.businessActionDialogVisible = false
})
.catch(() => {})
.finally(() => {
this.businessActionSubmitting = false
})
})
},
resetBusinessActionDialog() {
this.activeBusinessAction = null
this.businessActionBasePayload = {}
this.businessActionForm = {}
this.businessActionInputFields = []
},
hasBusinessActionValue(value) {
if (value === undefined || value === null) {
return false
}
return typeof value !== "string" || value.trim() !== ""
},
getBusinessActionFieldOptions(field) {
if (!field || !field.dictType) {
return []
}
return this._dict.getDictDataByType(field.dictType).map(option => ({
label: option.name,
value: option.value
}))
},
executeBusinessAction(action, payload) {
const method = String(action.method || "POST").toLowerCase()
const request = method === "get"
? this.request.get(action.path, { params: payload })
: this.request[method] ? this.request[method](action.path, payload) : this.request.post(action.path, payload)
return request.then(res => {
if (res.code !== "200") {
return Promise.reject(new Error(res.msg || "操作失败"))
}
this.#[[$message]]#.success(action.successMessage || "操作成功")
this.refreshBusinessActionTarget(action.refreshTarget)
return res
}).catch(error => {
const data = error && error.response ? error.response.data : null
this.#[[$message]]#.error((data && data.msg) || (error && error.message) || "操作失败")
return Promise.reject(error)
})
},
refreshBusinessActionTarget(refreshTarget) {
const target = refreshTarget || "list"
if (target === "none") {
return
}
if (target === "page") {
window.location.reload()
return
}
#if($isDetailPage)
this.loadDetail()
#else
this.load()
#end
},
buildBusinessActionPayload(action, row) {
const payload = {}
const currentUserFields = new Set(action.currentUserFields || [])
;(action.requestFields || []).forEach(field => {
if (currentUserFields.has(field)) {
return
}
const value = this.resolvePayloadValue(field, row)
if (value !== undefined) {
payload[field] = value
}
})
return payload
},
resolvePayloadValue(field, row) {
const camelField = this.toCamelCase(field)
const sources = [row, this.detailRecord, this.form, this.queryParams]
for (const source of sources) {
if (!source) {
continue
}
if (source[field] !== undefined) {
return source[field]
}
if (source[camelField] !== undefined) {
return source[camelField]
}
}
return undefined
},
toCamelCase(value) {
return String(value || "").replace(/_([a-zA-Z0-9])/g, (_, char) => char.toUpperCase())
},
handleSizeChange(pageSize) {
this.queryParams.pageSize = pageSize
this.load()
},
handleCurrentChange(pageNum) {
this.queryParams.pageNum = pageNum
this.load()
},
resolveResourceUrl(url) {
if (!url) {
return ""
}
if (url.indexOf("http://") === 0 || url.indexOf("https://") === 0 || url.indexOf("//") === 0 || url.indexOf("data:") === 0 || url.indexOf("blob:") === 0) {
return url
}
if (url.indexOf("/api/") === 0) {
return url
}
if (url.indexOf("/files/") === 0) {
return "/api" + url
}
return url
},
uploadSuccess(field, res) {
if (res && res.code === "200" && res.data && res.data.url) {
this.#[[$set]]#(this.form, field, res.data.url)
} else {
this.#[[$message]]#.error("上传失败,请重试")
}
}
}
}
</script>
<style scoped>
.portal-page {
display: grid;
gap: 22px;
}
.embedded-business-blocks {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 16px;
}
.embedded-business-block__item--span-12 { grid-column: span 12; }
.embedded-business-block__item--span-6 { grid-column: span 6; }
.embedded-business-block__item--span-4 { grid-column: span 4; }
.content-board {
display: grid;
grid-template-columns: minmax(0, 1.36fr) minmax(320px, 0.64fr);
gap: 24px;
align-items: stretch;
}
.feature-card {
position: relative;
min-height: 270px;
overflow: hidden;
background: #ffffff;
}
@media (max-width: 900px) {
.embedded-business-block__item {
grid-column: span 12;
}
}
.feature-illustration {
position: absolute;
inset: 0;
background:
linear-gradient(135deg, rgba(10, 66, 99, 0.92), rgba(255, 126, 86, 0.9)),
#184967;
}
.feature-illustration::before,
.feature-illustration::after {
position: absolute;
display: block;
content: "";
}
.feature-illustration::before {
left: 8%;
top: 18%;
width: 48%;
height: 42%;
border: 8px solid rgba(255, 255, 255, 0.8);
border-radius: 6px;
transform: rotate(-8deg);
}
.feature-illustration::after {
right: 9%;
bottom: -12%;
width: 290px;
height: 290px;
border-radius: 50%;
background:
radial-gradient(circle at 36% 38%, #ffffff 0 10px, transparent 11px),
radial-gradient(circle at 64% 38%, #ffffff 0 10px, transparent 11px),
radial-gradient(circle at 50% 58%, transparent 0 38px, #153d58 39px 45px, transparent 46px),
#39a6d8;
box-shadow: 0 20px 0 #133c58;
}
.device-face {
position: absolute;
left: 24%;
bottom: 20%;
display: grid;
grid-template-columns: repeat(2, 36px);
gap: 18px;
padding: 18px;
border-radius: 48px;
background: rgba(21, 62, 88, 0.9);
transform: rotate(7deg);
}
.device-face span {
width: 36px;
height: 36px;
border: 5px solid #f7ebe0;
border-radius: 50%;
}
.feature-copy {
position: absolute;
left: 28px;
bottom: 24px;
max-width: 420px;
color: #ffffff;
}
.feature-copy span {
display: inline-block;
margin-bottom: 8px;
color: #ffe1b6;
font-size: 13px;
font-weight: 700;
}
.feature-copy h2,
.feature-copy p,
.panel-heading h2 {
margin: 0;
letter-spacing: 0;
}
.feature-copy h2 {
font-size: 28px;
line-height: 1.25;
}
.feature-copy p {
margin-top: 8px;
color: rgba(255, 255, 255, 0.88);
line-height: 1.7;
}
.notice-panel {
min-width: 0;
background: #ffffff;
}
.notice-tabs {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
border-bottom: 1px solid #edf0f5;
}
.notice-tabs button {
position: relative;
min-height: 38px;
border: 0;
color: #475569;
background: transparent;
cursor: pointer;
}
.notice-tabs button.active {
color: #19a6ad;
font-weight: 700;
}
.notice-tabs button.active::after {
position: absolute;
right: 8px;
bottom: -1px;
left: 8px;
height: 2px;
background: #19a6ad;
content: "";
}
.notice-list {
display: grid;
gap: 18px;
margin: 16px 0 0;
padding: 0;
list-style: none;
}
.notice-list li {
display: grid;
gap: 7px;
}
.notice-list strong {
color: #172033;
font-size: 15px;
font-weight: 500;
line-height: 1.55;
}
.notice-list span {
display: inline-flex;
align-items: center;
gap: 6px;
color: #7b8493;
font-size: 13px;
}
.data-panel,
.notice-panel {
padding: 20px;
border: 1px solid #edf0f5;
border-radius: 4px;
}
.data-panel {
background: #ffffff;
}
.split-data-panel {
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
gap: 16px;
}
.split-data-panel .panel-heading {
grid-column: 1 / -1;
margin-bottom: 0;
}
.split-data-panel .filter-bar {
align-content: flex-start;
align-self: start;
flex-direction: column;
margin-bottom: 0;
padding: 14px;
border: 1px solid #edf0f5;
border-radius: 4px;
background: #f8fafc;
}
.split-data-panel .filter-item,
.split-data-panel .filter-bar .el-button {
width: 100%;
}
.split-data-panel .resource-table,
.split-data-panel .resource-card-grid,
.split-data-panel .pagination {
grid-column: 2;
}
.panel-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
.section-kicker {
color: #19a6ad;
font-size: 13px;
font-weight: 700;
}
.panel-heading h2 {
margin-top: 6px;
color: #172033;
font-size: 22px;
}
.panel-actions,
.filter-bar {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.filter-bar {
margin-bottom: 14px;
}
.filter-item {
width: 220px;
}
.resource-table {
width: 100%;
}
.resource-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 14px;
}
.resource-card-item {
position: relative;
display: grid;
gap: 12px;
min-height: 180px;
padding: 16px;
border: 1px solid #edf0f5;
border-radius: 4px;
background: #ffffff;
}
.resource-card-check {
position: absolute;
top: 12px;
right: 12px;
}
.resource-card-body {
display: grid;
gap: 10px;
}
.resource-card-field {
display: grid;
gap: 4px;
}
.resource-card-title {
margin: 0;
color: #172033;
font-size: 17px;
line-height: 1.35;
}
.resource-card-field span {
color: #7b8493;
font-size: 12px;
}
.resource-card-field strong {
color: #172033;
font-size: 14px;
font-weight: 600;
line-height: 1.45;
word-break: break-word;
}
.resource-card-image {
width: 100%;
height: 132px;
border-radius: 4px;
object-fit: cover;
}
.resource-card-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 4px;
border-top: 1px solid #edf0f5;
}
.detail-view {
min-height: 180px;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 14px 18px;
margin: 0;
}
.detail-field {
display: grid;
gap: 6px;
padding: 14px 0;
border-bottom: 1px solid #edf0f5;
}
.detail-field.wide {
grid-column: 1 / -1;
}
.detail-field dt {
color: #7b8493;
font-size: 13px;
font-weight: 600;
}
.detail-field dd {
margin: 0;
color: #172033;
font-size: 15px;
line-height: 1.65;
word-break: break-word;
}
.detail-image {
width: 180px;
height: 128px;
border-radius: 4px;
object-fit: cover;
}
.detail-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 18px;
padding-top: 16px;
border-top: 1px solid #edf0f5;
}
.empty-detail {
display: flex;
align-items: center;
gap: 8px;
min-height: 120px;
color: #7b8493;
}
.pagination {
margin-top: 14px;
text-align: right;
}
.danger-text {
color: #f56c6c;
}
.table-image {
width: 52px;
height: 52px;
border-radius: 4px;
}
.avatar-uploader .avatar {
display: block;
width: 96px;
height: 96px;
object-fit: cover;
}
.avatar-uploader-icon {
width: 96px;
height: 96px;
border: 1px dashed #d9d9d9;
border-radius: 4px;
color: #8c939d;
line-height: 96px;
text-align: center;
}
.file-link {
margin-top: 6px;
color: #606266;
word-break: break-all;
}
.business-action-hint {
margin: 0 0 18px;
padding: 10px 12px;
border-radius: 4px;
color: #606266;
background: #f5f7fa;
line-height: 1.6;
}
.business-action-hint + .el-form .el-date-editor,
.business-action-hint + .el-form .el-select,
.business-action-hint + .el-form .el-input,
.el-dialog .el-form .el-date-editor,
.el-dialog .el-form .el-select {
width: 100%;
}
@media (max-width: 980px) {
.content-board {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.split-data-panel {
grid-template-columns: 1fr;
}
.split-data-panel .panel-heading,
.split-data-panel .resource-table,
.split-data-panel .resource-card-grid,
.split-data-panel .pagination {
grid-column: 1;
}
.panel-heading {
align-items: flex-start;
flex-direction: column;
}
.filter-item {
width: 100%;
}
.notice-tabs {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
', 'admin', sysdate()),
(920312, 9203, 'frontend-login.vue.vm', NULL, 'frontend-login.vue.vm', '#set($portalProjectName = $projectName)
#if(!$portalProjectName || $portalProjectName == "")
#set($portalProjectName = $functionName)
#end
#if(!$portalProjectName || $portalProjectName == "")
#set($portalProjectName = "Portal")
#end
#set($portalDefaultPath = $defaultFrontendPath)
#if(!$portalDefaultPath || $portalDefaultPath == "")
#set($portalDefaultPath = "/${moduleName}/${businessName}")
#end
<template>
<section class="portal-login">
<div class="login-visual" aria-hidden="true">
<div class="login-brand">
<span class="login-logo"></span>
<strong>${portalProjectName}</strong>
</div>
<h1>${portalProjectName}</h1>
<p>访</p>
</div>
<div class="login-panel">
<div class="login-heading">
<span></span>
<h2></h2>
</div>
<el-form :model="form" label-position="top" class="login-form" @submit.native.prevent>
<el-form-item label="用户名">
<el-input v-model="form.username" placeholder="请输入用户名" prefix-icon="el-icon-user" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" placeholder="请输入密码" prefix-icon="el-icon-lock" show-password />
</el-form-item>
<el-button type="primary" class="login-button" :loading="loading" @click="handleLogin"></el-button>
</el-form>
</div>
</section>
</template>
<script>
export default {
name: "PortalLogin",
data() {
return {
loading: false,
form: {
username: "user",
password: "123456"
}
}
},
methods: {
handleLogin() {
if (!this.form.username || !this.form.password) {
this.#[[$message]]#.warning("请输入用户名和密码")
return
}
this.loading = true
this.request.post("/auth/login", this.form)
.then(res => {
if (!res || res.code !== "200" || !res.data || !res.data.token) {
throw new Error((res && res.msg) || "登录失败")
}
localStorage.setItem("portal-token", res.data.token)
localStorage.setItem("portal-user", JSON.stringify(res.data.user || {
username: this.form.username,
nickname: this.form.username
}))
window.dispatchEvent(new Event("portal-user-change"))
this.#[[$message]]#.success("登录成功")
const redirect = this.#[[$route]]#.query.redirect || "${portalDefaultPath}"
this.#[[$router]]#.push(redirect)
})
.catch(error => {
this.#[[$message]]#.error(error.message || "登录失败")
})
.finally(() => {
this.loading = false
})
}
}
}
</script>
<style scoped>
.portal-login {
display: grid;
grid-template-columns: minmax(0, 1fr) 430px;
min-height: 100vh;
background:
linear-gradient(132deg, rgba(24, 141, 149, 0.95), rgba(44, 120, 215, 0.88)),
#168d95;
}
.login-visual {
position: relative;
display: flex;
min-width: 0;
flex-direction: column;
justify-content: center;
overflow: hidden;
padding: 72px 9vw;
color: #ffffff;
}
.login-visual::before {
position: absolute;
inset: 0;
display: block;
content: "";
opacity: 0.58;
background:
linear-gradient(42deg, transparent 0 18%, rgba(255, 255, 255, 0.18) 18% 27%, transparent 27% 100%),
linear-gradient(132deg, transparent 0 48%, rgba(246, 186, 51, 0.32) 48% 55%, transparent 55% 100%);
}
.login-brand,
.login-visual h1,
.login-visual p {
position: relative;
z-index: 1;
}
.login-brand {
display: inline-flex;
align-items: center;
gap: 12px;
margin-bottom: 46px;
font-size: 22px;
}
.login-logo {
width: 28px;
height: 28px;
border-radius: 8px 18px 8px 18px;
background: linear-gradient(135deg, #38d8f0 0 35%, #f3ba28 35% 66%, #5bc269 66%);
transform: rotate(45deg);
}
.login-visual h1,
.login-visual p {
margin: 0;
}
.login-visual h1 {
max-width: 680px;
font-size: 52px;
font-weight: 600;
letter-spacing: 0;
line-height: 1.18;
}
.login-visual p {
max-width: 520px;
margin-top: 20px;
color: rgba(255, 255, 255, 0.88);
font-size: 17px;
line-height: 1.8;
}
.login-panel {
display: flex;
flex-direction: column;
justify-content: center;
padding: 48px;
background: #ffffff;
}
.login-heading span {
color: #168d95;
font-size: 14px;
font-weight: 700;
}
.login-heading h2 {
margin: 8px 0 28px;
color: #111827;
font-size: 30px;
font-weight: 700;
letter-spacing: 0;
}
.login-form {
display: grid;
gap: 2px;
}
.login-button {
width: 100%;
height: 42px;
margin-top: 6px;
border-color: #168d95;
background: #168d95;
}
@media (max-width: 880px) {
.portal-login {
grid-template-columns: 1fr;
}
.login-visual {
min-height: 280px;
padding: 42px 26px;
}
.login-visual h1 {
font-size: 36px;
}
.login-panel {
padding: 34px 24px 44px;
}
}
</style>
', 'admin', sysdate()),
(920313, 9203, 'frontend-dict.js.vm', NULL, 'frontend-dict.js.vm', 'const emptyDict = []
export default {
getDictDataByType() {
return emptyDict
},
getDictLabel(type, value) {
return value === undefined || value === null || value === "" ? "-" : value
}
}
', 'admin', sysdate());
-- Backend project structure
INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(920100, 0, '{projectName}-backend', 'folder', NULL, 9201, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(920101, 920100, 'pom.xml', 'file', NULL, 9201, -1, 'pom.xml.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920102, 920100, 'README.md', 'file', NULL, 9201, -1, 'README.md.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920103, 920100, 'sql', 'folder', NULL, 9201, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL),
(920104, 920103, '{businessName}.sql', 'file', NULL, 9201, 0, 'sql.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920110, 920100, 'src', 'folder', NULL, 9201, NULL, NULL, 4, '0', 'admin', sysdate(), '', NULL),
(920111, 920110, 'main', 'folder', NULL, 9201, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(920112, 920111, 'java', 'folder', NULL, 9201, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(920113, 920111, 'resources', 'folder', NULL, 9201, NULL, NULL, 2, '0', 'admin', sysdate(), '', NULL),
(920114, 920112, '{packagePath}', 'folder', NULL, 9201, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(920115, 920114, '{projectClassName}Application.java', 'file', NULL, 9201, -1, 'Application.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920116, 920114, 'common', 'folder', NULL, 9201, NULL, NULL, 2, '0', 'admin', sysdate(), '', NULL),
(920117, 920116, 'Result.java', 'file', NULL, 9201, -1, 'Result.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920118, 920114, 'config', 'folder', NULL, 9201, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL),
(920119, 920118, 'MybatisPlusConfig.java', 'file', NULL, 9201, -1, 'MybatisPlusConfig.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920120, 920118, 'CorsConfig.java', 'file', NULL, 9201, -1, 'CorsConfig.java.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920121, 920114, 'controller', 'folder', NULL, 9201, NULL, NULL, 4, '0', 'admin', sysdate(), '', NULL),
(920122, 920121, 'FileController.java', 'file', NULL, 9201, -1, 'FileController.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920123, 920121, '{ClassName}Controller.java', 'file', NULL, 9201, 0, 'controller.java.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920124, 920114, 'entity', 'folder', NULL, 9201, NULL, NULL, 5, '0', 'admin', sysdate(), '', NULL),
(920125, 920124, '{ClassName}.java', 'file', NULL, 9201, 0, 'entity.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920126, 920114, 'mapper', 'folder', NULL, 9201, NULL, NULL, 6, '0', 'admin', sysdate(), '', NULL),
(920127, 920126, '{ClassName}Mapper.java', 'file', NULL, 9201, 0, 'mapper.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920128, 920114, 'service', 'folder', NULL, 9201, NULL, NULL, 7, '0', 'admin', sysdate(), '', NULL),
(920129, 920128, 'I{ClassName}Service.java', 'file', NULL, 9201, 0, 'service.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920130, 920128, 'impl', 'folder', NULL, 9201, NULL, NULL, 2, '0', 'admin', sysdate(), '', NULL),
(920131, 920130, '{ClassName}ServiceImpl.java', 'file', NULL, 9201, 0, 'serviceImpl.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920132, 920113, 'application.yml', 'file', NULL, 9201, -1, 'application.yml.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920133, 920113, 'mapper', 'folder', NULL, 9201, NULL, NULL, 2, '0', 'admin', sysdate(), '', NULL),
(920134, 920133, '{moduleName}', 'folder', NULL, 9201, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(920135, 920134, '{ClassName}Mapper.xml', 'file', NULL, 9201, 0, 'mapper.xml.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920136, 920124, 'PortalUser.java', 'file', NULL, 9201, -1, 'PortalUser.java.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920137, 920126, 'PortalUserMapper.java', 'file', NULL, 9201, -1, 'PortalUserMapper.java.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920138, 920114, 'security', 'folder', NULL, 9201, NULL, NULL, 8, '0', 'admin', sysdate(), '', NULL),
(920139, 920138, 'PortalAuthTokenStore.java', 'file', NULL, 9201, -1, 'PortalAuthTokenStore.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920140, 920138, 'PortalAuthInterceptor.java', 'file', NULL, 9201, -1, 'PortalAuthInterceptor.java.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920141, 920121, 'PortalAuthController.java', 'file', NULL, 9201, -1, 'PortalAuthController.java.vm', 3, '0', 'admin', sysdate(), '', NULL),
(920142, 920124, 'AdminUser.java', 'file', NULL, 9201, -1, 'AdminUser.java.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920143, 920126, 'AdminUserMapper.java', 'file', NULL, 9201, -1, 'AdminUserMapper.java.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920144, 920121, 'AdminAuthController.java', 'file', NULL, 9201, -1, 'AdminAuthController.java.vm', 4, '0', 'admin', sysdate(), '', NULL);
-- Admin frontend project structure
INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(920200, 0, '{projectName}-admin', 'folder', NULL, 9202, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(920201, 920200, 'package.json', 'file', NULL, 9202, -1, 'package.json.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920202, 920200, 'babel.config.js', 'file', NULL, 9202, -1, 'babel.config.js.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920203, 920200, 'vue.config.js', 'file', NULL, 9202, -1, 'vue.config.js.vm', 3, '0', 'admin', sysdate(), '', NULL),
(920204, 920200, 'public', 'folder', NULL, 9202, NULL, NULL, 4, '0', 'admin', sysdate(), '', NULL),
(920205, 920204, 'index.html', 'file', NULL, 9202, -1, 'index.html.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920206, 920200, 'src', 'folder', NULL, 9202, NULL, NULL, 5, '0', 'admin', sysdate(), '', NULL),
(920207, 920206, 'main.js', 'file', NULL, 9202, -1, 'admin-main.js.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920208, 920206, 'App.vue', 'file', NULL, 9202, -1, 'admin-App.vue.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920209, 920206, 'router', 'folder', NULL, 9202, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL),
(920210, 920209, 'index.js', 'file', NULL, 9202, -1, 'admin-router.js.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920211, 920206, 'utils', 'folder', NULL, 9202, NULL, NULL, 4, '0', 'admin', sysdate(), '', NULL),
(920212, 920211, 'request.js', 'file', NULL, 9202, -1, 'request.js.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920213, 920211, 'dict.js', 'file', NULL, 9202, -1, 'dict.js.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920214, 920206, 'views', 'folder', NULL, 9202, NULL, NULL, 5, '0', 'admin', sysdate(), '', NULL),
(920218, 920214, 'login', 'folder', NULL, 9202, -1, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(920219, 920218, 'index.vue', 'file', NULL, 9202, -1, 'admin-login.vue.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920215, 920214, '{moduleName}', 'folder', NULL, 9202, 0, NULL, 2, '0', 'admin', sysdate(), '', NULL),
(920216, 920215, '{businessName}', 'folder', NULL, 9202, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(920217, 920216, 'index.vue', 'file', NULL, 9202, 0, 'admin-index.vue.vm', 1, '0', 'admin', sysdate(), '', NULL);
-- Portal frontend project structure
INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(920300, 0, '{projectName}-web', 'folder', NULL, 9203, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(920301, 920300, 'package.json', 'file', NULL, 9203, -1, 'package.json.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920302, 920300, 'babel.config.js', 'file', NULL, 9203, -1, 'babel.config.js.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920303, 920300, 'vue.config.js', 'file', NULL, 9203, -1, 'vue.config.js.vm', 3, '0', 'admin', sysdate(), '', NULL),
(920304, 920300, 'public', 'folder', NULL, 9203, NULL, NULL, 4, '0', 'admin', sysdate(), '', NULL),
(920305, 920304, 'index.html', 'file', NULL, 9203, -1, 'index.html.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920306, 920300, 'src', 'folder', NULL, 9203, NULL, NULL, 5, '0', 'admin', sysdate(), '', NULL),
(920307, 920306, 'main.js', 'file', NULL, 9203, -1, 'frontend-main.js.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920308, 920306, 'App.vue', 'file', NULL, 9203, -1, 'frontend-App.vue.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920309, 920306, 'style.css', 'file', NULL, 9203, -1, 'style.css.vm', 3, '0', 'admin', sysdate(), '', NULL),
(920310, 920306, 'router', 'folder', NULL, 9203, NULL, NULL, 4, '0', 'admin', sysdate(), '', NULL),
(920311, 920310, 'index.js', 'file', NULL, 9203, -1, 'frontend-router.js.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920312, 920306, 'api', 'folder', NULL, 9203, NULL, NULL, 5, '0', 'admin', sysdate(), '', NULL),
(920313, 920312, 'request.js', 'file', NULL, 9203, -1, 'frontend-request.js.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920314, 920312, '{businessName}.js', 'file', NULL, 9203, 0, 'frontend-api.js.vm', 2, '0', 'admin', sysdate(), '', NULL),
(920321, 920306, 'utils', 'folder', NULL, 9203, NULL, NULL, 6, '0', 'admin', sysdate(), '', NULL),
(920322, 920321, 'dict.js', 'file', NULL, 9203, -1, 'frontend-dict.js.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920315, 920306, 'views', 'folder', NULL, 9203, NULL, NULL, 7, '0', 'admin', sysdate(), '', NULL),
(920316, 920315, '{moduleName}', 'folder', NULL, 9203, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(920317, 920316, '{businessName}', 'folder', NULL, 9203, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(920318, 920317, 'index.vue', 'file', NULL, 9203, 0, 'frontend-index.vue.vm', 1, '0', 'admin', sysdate(), '', NULL),
(920319, 920315, 'login', 'folder', NULL, 9203, NULL, NULL, 2, '0', 'admin', sysdate(), '', NULL),
(920320, 920319, 'index.vue', 'file', NULL, 9203, -1, 'frontend-login.vue.vm', 1, '0', 'admin', sysdate(), '', NULL);
SET FOREIGN_KEY_CHECKS = 1;