2210 lines
74 KiB
Markdown
2210 lines
74 KiB
Markdown
# JeecgBoot 代码生成参考模板
|
||
|
||
本文档包含 JeecgBoot CRUD 代码的完整模板骨架,用 `{{变量}}` 标注替换位置。
|
||
|
||
## 变量说明
|
||
|
||
| 变量 | 说明 | 示例 |
|
||
|------|------|------|
|
||
| `{{tableName}}` | 数据库表名 | `biz_goods` |
|
||
| `{{entityName}}` | 实体类名(PascalCase) | `BizGoods` |
|
||
| `{{entityName_uncap}}` | 实体变量名(camelCase) | `bizGoods` |
|
||
| `{{entityPackage}}` | 模块包名 | `biz` |
|
||
| `{{entityPackagePath}}` | URL路径(同entityPackage或含/) | `biz` |
|
||
| `{{description}}` | 功能描述 | `商品管理` |
|
||
| `{{today}}` | 生成日期 | `2026-03-11` |
|
||
| `{{timestamp}}` | 13位毫秒级真实时间戳(用于菜单ID,通过`date +%s%3N`获取) | `1741704000123` |
|
||
| `{{moduleRoot}}` | 后端模块根路径 | `jeecg-module-system/jeecg-system-biz` |
|
||
| `{{viewDir}}` | 前端视图目录 | `biz/goods` |
|
||
|
||
---
|
||
|
||
## A. 单表模式
|
||
|
||
### 生成文件清单
|
||
|
||
**后端 6 个文件:**
|
||
1. `{{moduleRoot}}/src/main/java/org/jeecg/modules/{{entityPackage}}/entity/{{entityName}}.java`
|
||
2. `{{moduleRoot}}/src/main/java/org/jeecg/modules/{{entityPackage}}/controller/{{entityName}}Controller.java`
|
||
3. `{{moduleRoot}}/src/main/java/org/jeecg/modules/{{entityPackage}}/service/I{{entityName}}Service.java`
|
||
4. `{{moduleRoot}}/src/main/java/org/jeecg/modules/{{entityPackage}}/service/impl/{{entityName}}ServiceImpl.java`
|
||
5. `{{moduleRoot}}/src/main/java/org/jeecg/modules/{{entityPackage}}/mapper/{{entityName}}Mapper.java`
|
||
6. `{{moduleRoot}}/src/main/java/org/jeecg/modules/{{entityPackage}}/mapper/xml/{{entityName}}Mapper.xml`
|
||
|
||
**前端 - vue3 封装风格(4个文件):**
|
||
7. `src/views/{{viewDir}}/{{entityName}}.api.ts`
|
||
8. `src/views/{{viewDir}}/{{entityName}}.data.ts`
|
||
9. `src/views/{{viewDir}}/{{entityName}}List.vue`
|
||
10. `src/views/{{viewDir}}/components/{{entityName}}Modal.vue`
|
||
|
||
**前端 - vue3Native 原生风格(5个文件):**
|
||
7. `src/views/{{viewDir}}/{{entityName}}.api.ts`
|
||
8. `src/views/{{viewDir}}/{{entityName}}.data.ts`
|
||
9. `src/views/{{viewDir}}/{{entityName}}List.vue`
|
||
10. `src/views/{{viewDir}}/components/{{entityName}}Modal.vue`
|
||
11. `src/views/{{viewDir}}/components/{{entityName}}Form.vue`
|
||
|
||
**SQL(1个文件):**
|
||
12. Flyway SQL: `jeecg-module-system/jeecg-system-start/src/main/resources/db/flyway/V{{version}}__{{description}}.sql`
|
||
|
||
---
|
||
|
||
### A1. Entity.java
|
||
|
||
```java
|
||
package org.jeecg.modules.{{entityPackage}}.entity;
|
||
|
||
import java.io.Serializable;
|
||
import java.util.Date;
|
||
import java.math.BigDecimal;
|
||
import com.baomidou.mybatisplus.annotation.IdType;
|
||
import com.baomidou.mybatisplus.annotation.TableId;
|
||
import com.baomidou.mybatisplus.annotation.TableName;
|
||
import lombok.Data;
|
||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||
import org.springframework.format.annotation.DateTimeFormat;
|
||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||
import org.jeecg.common.aspect.annotation.Dict;
|
||
import io.swagger.v3.oas.annotations.media.Schema;
|
||
import lombok.EqualsAndHashCode;
|
||
import lombok.experimental.Accessors;
|
||
|
||
/**
|
||
* @Description: {{description}}
|
||
* @Author: jeecg-boot
|
||
* @Date: {{today}}
|
||
* @Version: V1.0
|
||
*/
|
||
@Data
|
||
@TableName("{{tableName}}")
|
||
@Accessors(chain = true)
|
||
@EqualsAndHashCode(callSuper = false)
|
||
@Schema(description = "{{description}}")
|
||
public class {{entityName}} implements Serializable {
|
||
private static final long serialVersionUID = 1L;
|
||
|
||
// === 主键字段(根据表DDL自适应) ===
|
||
|
||
// --- 方式1: JeecgBoot 标准字符串主键 (varchar(36)/varchar(32), 无AUTO_INCREMENT) ---
|
||
// @TableId(type = IdType.ASSIGN_ID)
|
||
// @Schema(description = "主键")
|
||
// private String id;
|
||
|
||
// --- 方式2: int 自增主键 (int AUTO_INCREMENT) ---
|
||
// @TableId(type = IdType.AUTO)
|
||
// @Schema(description = "主键")
|
||
// private Integer id;
|
||
|
||
// --- 方式3: bigint 自增主键 (bigint AUTO_INCREMENT) ---
|
||
// @TableId(type = IdType.AUTO)
|
||
// @Schema(description = "主键")
|
||
// private Long id;
|
||
|
||
// --- 方式4: bigint 雪花ID (bigint, 无AUTO_INCREMENT) ---
|
||
// @TableId(type = IdType.ASSIGN_ID)
|
||
// @Schema(description = "主键")
|
||
// private Long id;
|
||
|
||
// === 业务字段(根据需求生成) ===
|
||
// 每个业务字段按以下规则生成注解:
|
||
|
||
// --- String 字段 ---
|
||
// @Excel(name = "字段注释", width = 15)
|
||
// private String fieldName;
|
||
|
||
// --- 带字典的 String 字段 ---
|
||
// @Excel(name = "字段注释", width = 15, dicCode = "dict_code")
|
||
// @Dict(dicCode = "dict_code")
|
||
// private String fieldName;
|
||
|
||
// --- 关联表字典的 String 字段 ---
|
||
// @Excel(name = "字段注释", width = 15, dictTable = "sys_user", dicText = "realname", dicCode = "username")
|
||
// @Dict(dictTable = "sys_user", dicText = "realname", dicCode = "username")
|
||
// private String fieldName;
|
||
|
||
// --- Integer 字段 ---
|
||
// @Excel(name = "字段注释", width = 15)
|
||
// private Integer fieldName;
|
||
|
||
// --- BigDecimal 字段 ---
|
||
// @Excel(name = "字段注释", width = 15)
|
||
// private BigDecimal fieldName;
|
||
|
||
// --- Date 字段 ---
|
||
// @Excel(name = "字段注释", width = 15, format = "yyyy-MM-dd")
|
||
// @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
|
||
// @DateTimeFormat(pattern = "yyyy-MM-dd")
|
||
// private Date fieldName;
|
||
|
||
// --- DateTime 字段 ---
|
||
// @Excel(name = "字段注释", width = 20, format = "yyyy-MM-dd HH:mm:ss")
|
||
// @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||
// @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||
// private Date fieldName;
|
||
|
||
// === 系统字段(仅在表中实际存在时才生成,不要盲目添加!) ===
|
||
|
||
// --- 以下每个字段都需要检查表DDL中是否存在对应列,不存在则不生成 ---
|
||
|
||
// 如果表有 create_by 列:
|
||
// /**创建人*/
|
||
// @Schema(description = "创建人")
|
||
// private String createBy;
|
||
|
||
// 如果表有 create_time 列:
|
||
// /**创建日期*/
|
||
// @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||
// @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||
// @Schema(description = "创建日期")
|
||
// private Date createTime;
|
||
|
||
// 如果表有 update_by 列:
|
||
// /**更新人*/
|
||
// @Schema(description = "更新人")
|
||
// private String updateBy;
|
||
|
||
// 如果表有 update_time 列:
|
||
// /**更新日期*/
|
||
// @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||
// @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||
// @Schema(description = "更新日期")
|
||
// private Date updateTime;
|
||
|
||
// 如果表有 sys_org_code 列:
|
||
// /**所属部门*/
|
||
// @Schema(description = "所属部门")
|
||
// private String sysOrgCode;
|
||
|
||
// 新建表时默认添加全部系统字段;已有表按实际DDL决定。
|
||
}
|
||
```
|
||
|
||
**字典注解规则:**
|
||
- 下拉/单选/多选/搜索框 + 字典编码: `@Dict(dicCode = "xxx")`
|
||
- 下拉/单选/多选/搜索框 + 字典表: `@Dict(dictTable = "tableName", dicText = "textField", dicCode = "codeField")`
|
||
- 用户选择: `@Dict(dictTable = "sys_user", dicText = "realname", dicCode = "username")`
|
||
- 部门选择: `@Dict(dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")`
|
||
|
||
---
|
||
|
||
### A2. Controller.java
|
||
|
||
```java
|
||
package org.jeecg.modules.{{entityPackage}}.controller;
|
||
|
||
import java.util.Arrays;
|
||
import jakarta.servlet.http.HttpServletRequest;
|
||
import jakarta.servlet.http.HttpServletResponse;
|
||
import org.jeecg.common.api.vo.Result;
|
||
import org.jeecg.common.system.query.QueryGenerator;
|
||
import org.jeecg.common.util.oConvertUtils;
|
||
import org.jeecg.modules.{{entityPackage}}.entity.{{entityName}};
|
||
import org.jeecg.modules.{{entityPackage}}.service.I{{entityName}}Service;
|
||
|
||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
|
||
import org.jeecg.common.system.base.controller.JeecgController;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.web.bind.annotation.*;
|
||
import org.springframework.web.servlet.ModelAndView;
|
||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||
import io.swagger.v3.oas.annotations.Operation;
|
||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||
|
||
/**
|
||
* @Description: {{description}}
|
||
* @Author: jeecg-boot
|
||
* @Date: {{today}}
|
||
* @Version: V1.0
|
||
*/
|
||
@Tag(name = "{{description}}")
|
||
@RestController
|
||
@RequestMapping("/{{entityPackagePath}}/{{entityName_uncap}}")
|
||
@Slf4j
|
||
public class {{entityName}}Controller extends JeecgController<{{entityName}}, I{{entityName}}Service> {
|
||
@Autowired
|
||
private I{{entityName}}Service {{entityName_uncap}}Service;
|
||
|
||
/**
|
||
* 分页列表查询
|
||
*/
|
||
@Operation(summary = "{{description}}-分页列表查询")
|
||
@GetMapping(value = "/list")
|
||
public Result<IPage<{{entityName}}>> queryPageList({{entityName}} {{entityName_uncap}},
|
||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||
HttpServletRequest req) {
|
||
QueryWrapper<{{entityName}}> queryWrapper = QueryGenerator.initQueryWrapper({{entityName_uncap}}, req.getParameterMap());
|
||
Page<{{entityName}}> page = new Page<>(pageNo, pageSize);
|
||
IPage<{{entityName}}> pageList = {{entityName_uncap}}Service.page(page, queryWrapper);
|
||
return Result.OK(pageList);
|
||
}
|
||
|
||
/**
|
||
* 添加
|
||
*/
|
||
@AutoLog(value = "{{description}}-添加")
|
||
@Operation(summary = "{{description}}-添加")
|
||
@RequiresPermissions("{{entityPackage}}:{{tableName}}:add")
|
||
@PostMapping(value = "/add")
|
||
public Result<String> add(@RequestBody {{entityName}} {{entityName_uncap}}) {
|
||
{{entityName_uncap}}Service.save({{entityName_uncap}});
|
||
return Result.OK("添加成功!");
|
||
}
|
||
|
||
/**
|
||
* 编辑
|
||
*/
|
||
@AutoLog(value = "{{description}}-编辑")
|
||
@Operation(summary = "{{description}}-编辑")
|
||
@RequiresPermissions("{{entityPackage}}:{{tableName}}:edit")
|
||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||
public Result<String> edit(@RequestBody {{entityName}} {{entityName_uncap}}) {
|
||
{{entityName_uncap}}Service.updateById({{entityName_uncap}});
|
||
return Result.OK("编辑成功!");
|
||
}
|
||
|
||
/**
|
||
* 通过id删除
|
||
* 注意:参数类型需与Entity主键类型一致
|
||
* - String主键: @RequestParam(name = "id", required = true) String id
|
||
* - Integer主键: @RequestParam(name = "id", required = true) Integer id
|
||
* - Long主键: @RequestParam(name = "id", required = true) Long id
|
||
*/
|
||
@AutoLog(value = "{{description}}-通过id删除")
|
||
@Operation(summary = "{{description}}-通过id删除")
|
||
@RequiresPermissions("{{entityPackage}}:{{tableName}}:delete")
|
||
@DeleteMapping(value = "/delete")
|
||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||
{{entityName_uncap}}Service.removeById(id);
|
||
return Result.OK("删除成功!");
|
||
}
|
||
|
||
/**
|
||
* 批量删除
|
||
* 注意:当主键为 Integer/Long 时,需将 ids 转为对应类型的 List:
|
||
* - Integer: Arrays.stream(ids.split(",")).map(Integer::parseInt).collect(Collectors.toList())
|
||
* - Long: Arrays.stream(ids.split(",")).map(Long::parseLong).collect(Collectors.toList())
|
||
* - String: Arrays.asList(ids.split(","))
|
||
*/
|
||
@AutoLog(value = "{{description}}-批量删除")
|
||
@Operation(summary = "{{description}}-批量删除")
|
||
@RequiresPermissions("{{entityPackage}}:{{tableName}}:deleteBatch")
|
||
@DeleteMapping(value = "/deleteBatch")
|
||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||
this.{{entityName_uncap}}Service.removeByIds(Arrays.asList(ids.split(",")));
|
||
return Result.OK("批量删除成功!");
|
||
}
|
||
|
||
/**
|
||
* 通过id查询
|
||
* 注意:参数类型需与Entity主键类型一致(同 delete 方法)
|
||
*/
|
||
@Operation(summary = "{{description}}-通过id查询")
|
||
@GetMapping(value = "/queryById")
|
||
public Result<{{entityName}}> queryById(@RequestParam(name = "id", required = true) String id) {
|
||
{{entityName}} {{entityName_uncap}} = {{entityName_uncap}}Service.getById(id);
|
||
if ({{entityName_uncap}} == null) {
|
||
return Result.error("未找到对应数据");
|
||
}
|
||
return Result.OK({{entityName_uncap}});
|
||
}
|
||
|
||
/**
|
||
* 导出excel
|
||
*/
|
||
@RequiresPermissions("{{entityPackage}}:{{tableName}}:exportXls")
|
||
@RequestMapping(value = "/exportXls")
|
||
public ModelAndView exportXls(HttpServletRequest request, {{entityName}} {{entityName_uncap}}) {
|
||
return super.exportXls(request, {{entityName_uncap}}, {{entityName}}.class, "{{description}}");
|
||
}
|
||
|
||
/**
|
||
* 通过excel导入数据
|
||
*/
|
||
@RequiresPermissions("{{entityPackage}}:{{tableName}}:importExcel")
|
||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||
return super.importExcel(request, response, {{entityName}}.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
**注意:** 如果查询字段中有下拉/单选/多选/复选框类型的,需要添加自定义查询规则:
|
||
```java
|
||
// 在 queryPageList 方法中:
|
||
Map<String, QueryRuleEnum> customeRuleMap = new HashMap<>();
|
||
customeRuleMap.put("status", QueryRuleEnum.LIKE_WITH_OR);
|
||
QueryWrapper<{{entityName}}> queryWrapper = QueryGenerator.initQueryWrapper({{entityName_uncap}}, req.getParameterMap(), customeRuleMap);
|
||
```
|
||
|
||
---
|
||
|
||
### A3. IService.java
|
||
|
||
```java
|
||
package org.jeecg.modules.{{entityPackage}}.service;
|
||
|
||
import org.jeecg.modules.{{entityPackage}}.entity.{{entityName}};
|
||
import com.baomidou.mybatisplus.extension.service.IService;
|
||
|
||
/**
|
||
* @Description: {{description}}
|
||
* @Author: jeecg-boot
|
||
* @Date: {{today}}
|
||
* @Version: V1.0
|
||
*/
|
||
public interface I{{entityName}}Service extends IService<{{entityName}}> {
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### A4. ServiceImpl.java
|
||
|
||
```java
|
||
package org.jeecg.modules.{{entityPackage}}.service.impl;
|
||
|
||
import org.jeecg.modules.{{entityPackage}}.entity.{{entityName}};
|
||
import org.jeecg.modules.{{entityPackage}}.mapper.{{entityName}}Mapper;
|
||
import org.jeecg.modules.{{entityPackage}}.service.I{{entityName}}Service;
|
||
import org.springframework.stereotype.Service;
|
||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||
|
||
/**
|
||
* @Description: {{description}}
|
||
* @Author: jeecg-boot
|
||
* @Date: {{today}}
|
||
* @Version: V1.0
|
||
*/
|
||
@Service
|
||
public class {{entityName}}ServiceImpl extends ServiceImpl<{{entityName}}Mapper, {{entityName}}> implements I{{entityName}}Service {
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### A5. Mapper.java
|
||
|
||
```java
|
||
package org.jeecg.modules.{{entityPackage}}.mapper;
|
||
|
||
import org.jeecg.modules.{{entityPackage}}.entity.{{entityName}};
|
||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||
|
||
/**
|
||
* @Description: {{description}}
|
||
* @Author: jeecg-boot
|
||
* @Date: {{today}}
|
||
* @Version: V1.0
|
||
*/
|
||
public interface {{entityName}}Mapper extends BaseMapper<{{entityName}}> {
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### A6. Mapper.xml
|
||
|
||
```xml
|
||
<?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="org.jeecg.modules.{{entityPackage}}.mapper.{{entityName}}Mapper">
|
||
</mapper>
|
||
```
|
||
|
||
---
|
||
|
||
### A7. API文件 (vue3 和 vue3Native 通用)
|
||
|
||
```typescript
|
||
import { defHttp } from '/@/utils/http/axios';
|
||
import { useMessage } from '/@/hooks/web/useMessage';
|
||
|
||
const { createConfirm } = useMessage();
|
||
|
||
enum Api {
|
||
list = '/{{entityPackagePath}}/{{entityName_uncap}}/list',
|
||
save = '/{{entityPackagePath}}/{{entityName_uncap}}/add',
|
||
edit = '/{{entityPackagePath}}/{{entityName_uncap}}/edit',
|
||
deleteOne = '/{{entityPackagePath}}/{{entityName_uncap}}/delete',
|
||
deleteBatch = '/{{entityPackagePath}}/{{entityName_uncap}}/deleteBatch',
|
||
importExcel = '/{{entityPackagePath}}/{{entityName_uncap}}/importExcel',
|
||
exportXls = '/{{entityPackagePath}}/{{entityName_uncap}}/exportXls',
|
||
}
|
||
|
||
/**
|
||
* 导出api
|
||
*/
|
||
export const getExportUrl = Api.exportXls;
|
||
|
||
/**
|
||
* 导入api
|
||
*/
|
||
export const getImportUrl = Api.importExcel;
|
||
|
||
/**
|
||
* 列表接口
|
||
* @param params
|
||
*/
|
||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||
|
||
/**
|
||
* 删除单个
|
||
*/
|
||
export const deleteOne = (params, handleSuccess) => {
|
||
return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
|
||
handleSuccess();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 批量删除
|
||
* @param params
|
||
*/
|
||
export const batchDelete = (params, handleSuccess) => {
|
||
createConfirm({
|
||
iconType: 'warning',
|
||
title: '确认删除',
|
||
content: '是否删除选中数据',
|
||
okText: '确认',
|
||
cancelText: '取消',
|
||
onOk: () => {
|
||
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
|
||
handleSuccess();
|
||
});
|
||
},
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 保存或者更新
|
||
* @param params
|
||
* @param isUpdate
|
||
*/
|
||
export const saveOrUpdate = (params, isUpdate) => {
|
||
let url = isUpdate ? Api.edit : Api.save;
|
||
return defHttp.post({ url: url, params });
|
||
};
|
||
```
|
||
|
||
**vue3Native 风格差异:** `saveOrUpdate` 使用 `{ isTransformResponse: false }` 选项:
|
||
```typescript
|
||
export const saveOrUpdate = (params, isUpdate) => {
|
||
let url = isUpdate ? Api.edit : Api.save;
|
||
return defHttp.post({ url: url, params }, { isTransformResponse: false });
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
### A8. Data文件 - vue3 封装风格
|
||
|
||
```typescript
|
||
import { BasicColumn } from '/@/components/Table';
|
||
import { FormSchema } from '/@/components/Table';
|
||
import { rules } from '/@/utils/helper/validator';
|
||
import { render } from '/@/utils/common/renderUtils';
|
||
|
||
// 列表列定义
|
||
export const columns: BasicColumn[] = [
|
||
// --- 普通字符串列 ---
|
||
// {
|
||
// title: '字段名称',
|
||
// align: 'center',
|
||
// dataIndex: 'fieldName',
|
||
// },
|
||
|
||
// --- 日期列(截取前10位) ---
|
||
// {
|
||
// title: '日期',
|
||
// align: 'center',
|
||
// dataIndex: 'dateField',
|
||
// customRender: ({ text }) => {
|
||
// text = !text ? '' : (text.length > 10 ? text.substr(0, 10) : text);
|
||
// return text;
|
||
// },
|
||
// },
|
||
|
||
// --- 字典翻译列(自动渲染_dictText后缀) ---
|
||
// {
|
||
// title: '状态',
|
||
// align: 'center',
|
||
// dataIndex: 'status_dictText',
|
||
// },
|
||
|
||
// --- Switch 列 ---
|
||
// {
|
||
// title: '是否启用',
|
||
// align: 'center',
|
||
// dataIndex: 'enabled',
|
||
// customRender: ({ text }) => {
|
||
// return render.renderSwitch(text, [{ text: '是', value: 'Y' }, { text: '否', value: 'N' }]);
|
||
// },
|
||
// },
|
||
|
||
// --- 图片列 ---
|
||
// {
|
||
// title: '图片',
|
||
// align: 'center',
|
||
// dataIndex: 'imageField',
|
||
// customRender: render.renderImage,
|
||
// },
|
||
|
||
// --- 分类树列 ---
|
||
// {
|
||
// title: '分类',
|
||
// align: 'center',
|
||
// dataIndex: 'categoryField',
|
||
// customRender: ({ text }) => {
|
||
// return render.renderCategoryTree(text, 'categoryDictCode');
|
||
// },
|
||
// },
|
||
];
|
||
|
||
// 查询表单 Schema
|
||
export const searchFormSchema: FormSchema[] = [
|
||
// --- 文本查询 ---
|
||
// {
|
||
// label: '名称',
|
||
// field: 'name',
|
||
// component: 'JInput',
|
||
// colProps: { span: 6 },
|
||
// },
|
||
|
||
// --- 字典下拉查询 ---
|
||
// {
|
||
// label: '状态',
|
||
// field: 'status',
|
||
// component: 'JDictSelectTag',
|
||
// componentProps: { dictCode: 'dict_code' },
|
||
// colProps: { span: 6 },
|
||
// },
|
||
|
||
// --- 日期范围查询 ---
|
||
// {
|
||
// label: '创建日期',
|
||
// field: 'createTime',
|
||
// component: 'RangePicker',
|
||
// componentProps: { showTime: true },
|
||
// colProps: { span: 6 },
|
||
// },
|
||
];
|
||
|
||
// 编辑表单 Schema
|
||
export const formSchema: FormSchema[] = [
|
||
// 隐藏ID
|
||
{
|
||
label: '',
|
||
field: 'id',
|
||
component: 'Input',
|
||
show: false,
|
||
},
|
||
|
||
// --- 文本输入 ---
|
||
// {
|
||
// label: '名称',
|
||
// field: 'name',
|
||
// required: true,
|
||
// component: 'Input',
|
||
// componentProps: { placeholder: '请输入名称' },
|
||
// },
|
||
|
||
// --- 数字输入 ---
|
||
// {
|
||
// label: '数量',
|
||
// field: 'quantity',
|
||
// component: 'InputNumber',
|
||
// componentProps: { placeholder: '请输入数量' },
|
||
// },
|
||
|
||
// --- 字典下拉 ---
|
||
// {
|
||
// label: '状态',
|
||
// field: 'status',
|
||
// component: 'JDictSelectTag',
|
||
// componentProps: { dictCode: 'dict_code', placeholder: '请选择状态' },
|
||
// },
|
||
|
||
// --- 关联表字典下拉 ---
|
||
// {
|
||
// label: '类型',
|
||
// field: 'type',
|
||
// component: 'JDictSelectTag',
|
||
// componentProps: { dictCode: 'tableName,textField,codeField', placeholder: '请选择类型' },
|
||
// },
|
||
|
||
// --- Switch ---
|
||
// {
|
||
// label: '是否启用',
|
||
// field: 'enabled',
|
||
// component: 'JSwitch',
|
||
// componentProps: { options: ['Y', 'N'] },
|
||
// },
|
||
|
||
// --- 日期选择 ---
|
||
// {
|
||
// label: '日期',
|
||
// field: 'dateField',
|
||
// component: 'DatePicker',
|
||
// componentProps: { showTime: false, valueFormat: 'YYYY-MM-DD', placeholder: '请选择日期' },
|
||
// },
|
||
|
||
// --- 日期时间选择 ---
|
||
// {
|
||
// label: '日期时间',
|
||
// field: 'datetimeField',
|
||
// component: 'DatePicker',
|
||
// componentProps: { showTime: true, valueFormat: 'YYYY-MM-DD HH:mm:ss', placeholder: '请选择日期时间' },
|
||
// },
|
||
|
||
// --- 文本域 ---
|
||
// {
|
||
// label: '备注',
|
||
// field: 'remark',
|
||
// component: 'InputTextArea',
|
||
// componentProps: { placeholder: '请输入备注' },
|
||
// },
|
||
|
||
// --- 富文本编辑器 ---
|
||
// {
|
||
// label: '内容',
|
||
// field: 'content',
|
||
// component: 'JEditor',
|
||
// },
|
||
|
||
// --- 图片上传 ---
|
||
// {
|
||
// label: '图片',
|
||
// field: 'imageField',
|
||
// component: 'JImageUpload',
|
||
// },
|
||
|
||
// --- 文件上传 ---
|
||
// {
|
||
// label: '附件',
|
||
// field: 'fileField',
|
||
// component: 'JUpload',
|
||
// },
|
||
|
||
// --- 用户选择 ---
|
||
// {
|
||
// label: '负责人',
|
||
// field: 'userId',
|
||
// component: 'JSelectUserByDept',
|
||
// componentProps: { labelKey: 'realname' },
|
||
// },
|
||
|
||
// --- 部门选择 ---
|
||
// {
|
||
// label: '部门',
|
||
// field: 'deptId',
|
||
// component: 'JSelectDept',
|
||
// },
|
||
|
||
// --- 分类树选择 ---
|
||
// {
|
||
// label: '分类',
|
||
// field: 'categoryField',
|
||
// component: 'JCategorySelect',
|
||
// componentProps: { pcode: 'categoryDictCode' },
|
||
// },
|
||
|
||
// --- 搜索选择 ---
|
||
// {
|
||
// label: '搜索',
|
||
// field: 'searchField',
|
||
// component: 'JSearchSelect',
|
||
// componentProps: { dict: 'tableName,textField,codeField', placeholder: '请选择' },
|
||
// },
|
||
];
|
||
|
||
// 高级查询配置
|
||
export const superQuerySchema = {
|
||
// fieldName: { title: '字段名', order: 0, view: 'text' },
|
||
// status: { title: '状态', order: 1, view: 'list', dictCode: 'dict_code' },
|
||
// dateField: { title: '日期', order: 2, view: 'date' },
|
||
// datetimeField: { title: '日期时间', order: 3, view: 'datetime' },
|
||
// quantity: { title: '数量', order: 4, view: 'number' },
|
||
};
|
||
```
|
||
|
||
**高级查询 view 类型映射:**
|
||
- string → `text`
|
||
- int/double/BigDecimal → `number`
|
||
- date → `date`
|
||
- datetime → `datetime`
|
||
- 字典字段(list/radio/checkbox) → `list`, 带 `dictCode`
|
||
- 关联表字典 → `list_multi` 或 `sel_search`, 带 `dictTable/dictCode/dictText`
|
||
- switch → `radio`
|
||
- user_select → `sel_user`
|
||
- dept_select → `sel_depart`
|
||
|
||
---
|
||
|
||
### A9. List页面 - vue3 封装风格
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<!--引用表格-->
|
||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||
<!--插槽:table标题-->
|
||
<template #tableTitle>
|
||
<a-button type="primary" v-auth="'{{entityPackage}}:{{tableName}}:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
|
||
<a-button type="primary" v-auth="'{{entityPackage}}:{{tableName}}:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
|
||
<j-upload-button type="primary" v-auth="'{{entityPackage}}:{{tableName}}:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
|
||
<a-dropdown v-if="selectedRowKeys.length > 0">
|
||
<template #overlay>
|
||
<a-menu>
|
||
<a-menu-item key="1" @click="batchHandleDelete">
|
||
<Icon icon="ant-design:delete-outlined" />
|
||
删除
|
||
</a-menu-item>
|
||
</a-menu>
|
||
</template>
|
||
<a-button v-auth="'{{entityPackage}}:{{tableName}}:deleteBatch'">批量操作
|
||
<Icon icon="mdi:chevron-down" />
|
||
</a-button>
|
||
</a-dropdown>
|
||
<!-- 高级查询 -->
|
||
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
|
||
</template>
|
||
<!--操作栏-->
|
||
<template #action="{ record }">
|
||
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
|
||
</template>
|
||
<!--字段回显插槽-->
|
||
<template v-slot:bodyCell="{ column, record, index, text }">
|
||
<!-- 富文本回显 -->
|
||
<!-- <template v-if="column.dataIndex==='content'">
|
||
<div v-html="text"></div>
|
||
</template> -->
|
||
<!-- 文件下载 -->
|
||
<!-- <template v-if="column.dataIndex==='fileField'">
|
||
<span v-if="!text" style="font-size: 12px;font-style: italic;">无文件</span>
|
||
<a-button v-else :ghost="true" type="primary" preIcon="ant-design:download-outlined" size="small" @click="downloadFile(text)">下载</a-button>
|
||
</template> -->
|
||
</template>
|
||
</BasicTable>
|
||
<!-- 表单区域 -->
|
||
<{{entityName}}Modal @register="registerModal" @success="handleSuccess" />
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" name="{{entityPackage}}-{{entityName_uncap}}" setup>
|
||
import { ref, reactive } from 'vue';
|
||
import { BasicTable, useTable, TableAction } from '/@/components/Table';
|
||
import { useModal } from '/@/components/Modal';
|
||
import { useListPage } from '/@/hooks/system/useListPage';
|
||
import {{entityName}}Modal from './components/{{entityName}}Modal.vue';
|
||
import { columns, searchFormSchema, superQuerySchema } from './{{entityName}}.data';
|
||
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './{{entityName}}.api';
|
||
import { downloadFile } from '/@/utils/common/renderUtils';
|
||
|
||
const queryParam = reactive<any>({});
|
||
const checkedKeys = ref<Array<string | number>>([]);
|
||
// 注册 modal
|
||
const [registerModal, { openModal }] = useModal();
|
||
// 注册 table
|
||
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
|
||
tableProps: {
|
||
title: '{{description}}',
|
||
api: list,
|
||
columns,
|
||
canResize: true,
|
||
formConfig: {
|
||
schemas: searchFormSchema,
|
||
autoSubmitOnEnter: true,
|
||
showAdvancedButton: true,
|
||
fieldMapToNumber: [
|
||
// 数字/时间范围查询映射
|
||
// ['fieldName', ['fieldName_begin', 'fieldName_end']],
|
||
],
|
||
fieldMapToTime: [
|
||
// 日期范围查询映射
|
||
// ['dateField', ['dateField_begin', 'dateField_end'], 'YYYY-MM-DD'],
|
||
// ['datetimeField', ['datetimeField_begin', 'datetimeField_end'], 'YYYY-MM-DD HH:mm:ss'],
|
||
],
|
||
},
|
||
actionColumn: {
|
||
width: 120,
|
||
fixed: 'right',
|
||
},
|
||
beforeFetch: (params) => {
|
||
return Object.assign(params, queryParam);
|
||
},
|
||
},
|
||
exportConfig: {
|
||
name: '{{description}}',
|
||
url: getExportUrl,
|
||
params: queryParam,
|
||
},
|
||
importConfig: {
|
||
url: getImportUrl,
|
||
success: handleSuccess,
|
||
},
|
||
});
|
||
|
||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||
|
||
// 高级查询配置
|
||
const superQueryConfig = reactive(superQuerySchema);
|
||
|
||
/**
|
||
* 高级查询事件
|
||
*/
|
||
function handleSuperQuery(params) {
|
||
Object.keys(params).map((k) => {
|
||
queryParam[k] = params[k];
|
||
});
|
||
reload();
|
||
}
|
||
|
||
/**
|
||
* 新增事件
|
||
*/
|
||
function handleAdd() {
|
||
openModal(true, {
|
||
isUpdate: false,
|
||
showFooter: true,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 编辑事件
|
||
*/
|
||
function handleEdit(record: Recordable) {
|
||
openModal(true, {
|
||
record,
|
||
isUpdate: true,
|
||
showFooter: true,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 详情
|
||
*/
|
||
function handleDetail(record: Recordable) {
|
||
openModal(true, {
|
||
record,
|
||
isUpdate: true,
|
||
showFooter: false,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 删除事件
|
||
*/
|
||
async function handleDelete(record) {
|
||
await deleteOne({ id: record.id }, handleSuccess);
|
||
}
|
||
|
||
/**
|
||
* 批量删除事件
|
||
*/
|
||
async function batchHandleDelete() {
|
||
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
|
||
}
|
||
|
||
/**
|
||
* 成功回调
|
||
*/
|
||
function handleSuccess() {
|
||
(selectedRowKeys.value = []) && reload();
|
||
}
|
||
|
||
/**
|
||
* 操作栏
|
||
*/
|
||
function getTableAction(record) {
|
||
return [
|
||
{
|
||
label: '编辑',
|
||
onClick: handleEdit.bind(null, record),
|
||
auth: '{{entityPackage}}:{{tableName}}:edit',
|
||
},
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 下拉操作栏
|
||
*/
|
||
function getDropDownAction(record) {
|
||
return [
|
||
{
|
||
label: '详情',
|
||
onClick: handleDetail.bind(null, record),
|
||
},
|
||
{
|
||
label: '删除',
|
||
popConfirm: {
|
||
title: '是否确认删除',
|
||
confirm: handleDelete.bind(null, record),
|
||
placement: 'topLeft',
|
||
},
|
||
auth: '{{entityPackage}}:{{tableName}}:delete',
|
||
},
|
||
];
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
:deep(.ant-picker-range) {
|
||
width: 100%;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
### A10. Modal组件 - vue3 封装风格
|
||
|
||
```vue
|
||
<template>
|
||
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
|
||
<BasicForm @register="registerForm" name="{{entityName}}Form" />
|
||
</BasicModal>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, computed, unref } from 'vue';
|
||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||
import { formSchema } from '../{{entityName}}.data';
|
||
import { saveOrUpdate } from '../{{entityName}}.api';
|
||
|
||
// Emits声明
|
||
const emit = defineEmits(['register', 'success']);
|
||
const isUpdate = ref(true);
|
||
const isDetail = ref(false);
|
||
|
||
// 表单配置
|
||
const [registerForm, { setProps, resetFields, setFieldsValue, validate, scrollToField }] = useForm({
|
||
// 单列布局用 labelWidth: 150
|
||
labelWidth: 150,
|
||
schemas: formSchema,
|
||
showActionButtonGroup: false,
|
||
// 多列布局用 baseColProps: { span: 12 } (双列) 或 { span: 8 } (三列)
|
||
baseColProps: { span: 24 },
|
||
});
|
||
|
||
// 表单赋值
|
||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||
// 重置表单
|
||
await resetFields();
|
||
setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter });
|
||
isUpdate.value = !!data?.isUpdate;
|
||
isDetail.value = !!data?.showFooter;
|
||
if (unref(isUpdate)) {
|
||
// 表单赋值
|
||
await setFieldsValue({
|
||
...data.record,
|
||
});
|
||
}
|
||
// 隐藏底部时禁用整个表单
|
||
setProps({ disabled: !data?.showFooter });
|
||
});
|
||
|
||
// 设置标题
|
||
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑'));
|
||
|
||
// 表单提交事件
|
||
async function handleSubmit(v) {
|
||
try {
|
||
let values = await validate();
|
||
setModalProps({ confirmLoading: true });
|
||
// 提交表单
|
||
await saveOrUpdate(values, isUpdate.value);
|
||
// 关闭弹窗
|
||
closeModal();
|
||
// 刷新列表
|
||
emit('success');
|
||
} catch ({ errorFields }) {
|
||
if (errorFields) {
|
||
const firstField = errorFields[0];
|
||
if (firstField) {
|
||
scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
|
||
}
|
||
}
|
||
return Promise.reject(errorFields);
|
||
} finally {
|
||
setModalProps({ confirmLoading: false });
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
:deep(.ant-input-number) {
|
||
width: 100%;
|
||
}
|
||
:deep(.ant-calendar-picker) {
|
||
width: 100%;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
**宽度规则:**
|
||
- 单列表单(fieldRowNum=1): width=800, baseColProps={span:24}
|
||
- 双列表单(fieldRowNum=2): width=1000, baseColProps={span:12}
|
||
- 三列表单(fieldRowNum=3): width=1200, baseColProps={span:8}
|
||
- 四列表单(fieldRowNum=4): width=1280, baseColProps={span:6}
|
||
|
||
---
|
||
|
||
### A8N. Data文件 - vue3Native 原生风格
|
||
|
||
vue3Native 的 data.ts 只包含 columns 和 superQuerySchema,不包含 formSchema(表单在模板中直接写控件)。
|
||
|
||
```typescript
|
||
import { BasicColumn } from '/@/components/Table';
|
||
import { FormSchema } from '/@/components/Table';
|
||
import { rules } from '/@/utils/helper/validator';
|
||
import { render } from '/@/utils/common/renderUtils';
|
||
|
||
// 列表列定义(与 vue3 封装风格完全相同)
|
||
export const columns: BasicColumn[] = [
|
||
// ... 同 A8 的 columns
|
||
];
|
||
|
||
// 高级查询配置(与 vue3 封装风格完全相同)
|
||
export const superQuerySchema = {
|
||
// ... 同 A8 的 superQuerySchema
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
### A9N. List页面 - vue3Native 原生风格
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<!--引用表格-->
|
||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||
<!--插槽:table标题-->
|
||
<template #tableTitle>
|
||
<a-button type="primary" v-auth="'{{entityPackage}}:{{tableName}}:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
|
||
<a-button type="primary" v-auth="'{{entityPackage}}:{{tableName}}:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
|
||
<j-upload-button type="primary" v-auth="'{{entityPackage}}:{{tableName}}:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
|
||
<a-dropdown v-if="selectedRowKeys.length > 0">
|
||
<template #overlay>
|
||
<a-menu>
|
||
<a-menu-item key="1" @click="batchHandleDelete">
|
||
<Icon icon="ant-design:delete-outlined" />
|
||
删除
|
||
</a-menu-item>
|
||
</a-menu>
|
||
</template>
|
||
<a-button v-auth="'{{entityPackage}}:{{tableName}}:deleteBatch'">批量操作
|
||
<Icon icon="mdi:chevron-down" />
|
||
</a-button>
|
||
</a-dropdown>
|
||
<!-- 高级查询 -->
|
||
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
|
||
</template>
|
||
<!--操作栏-->
|
||
<template #action="{ record }">
|
||
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
|
||
</template>
|
||
<!--字段回显插槽(同 vue3 封装风格)-->
|
||
<template v-slot:bodyCell="{ column, record, index, text }">
|
||
</template>
|
||
</BasicTable>
|
||
<!-- 表单区域 -->
|
||
<{{entityName}}Modal ref="registerModal" @success="handleSuccess" />
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" name="{{entityPackage}}-{{entityName_uncap}}" setup>
|
||
import { ref, reactive } from 'vue';
|
||
import { BasicTable, useTable, TableAction } from '/@/components/Table';
|
||
import { useListPage } from '/@/hooks/system/useListPage';
|
||
import {{entityName}}Modal from './components/{{entityName}}Modal.vue';
|
||
import { columns, superQuerySchema } from './{{entityName}}.data';
|
||
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './{{entityName}}.api';
|
||
import { downloadFile } from '/@/utils/common/renderUtils';
|
||
|
||
const queryParam = reactive<any>({});
|
||
const registerModal = ref();
|
||
|
||
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
|
||
tableProps: {
|
||
title: '{{description}}',
|
||
api: list,
|
||
columns,
|
||
canResize: true,
|
||
useSearchForm: false,
|
||
actionColumn: {
|
||
width: 120,
|
||
fixed: 'right',
|
||
},
|
||
beforeFetch: (params) => {
|
||
return Object.assign(params, queryParam);
|
||
},
|
||
},
|
||
exportConfig: {
|
||
name: '{{description}}',
|
||
url: getExportUrl,
|
||
params: queryParam,
|
||
},
|
||
importConfig: {
|
||
url: getImportUrl,
|
||
success: handleSuccess,
|
||
},
|
||
});
|
||
|
||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||
|
||
// 高级查询配置
|
||
const superQueryConfig = reactive(superQuerySchema);
|
||
|
||
function handleSuperQuery(params) {
|
||
Object.keys(params).map((k) => {
|
||
queryParam[k] = params[k];
|
||
});
|
||
reload();
|
||
}
|
||
|
||
function handleAdd() {
|
||
registerModal.value.disableSubmit = false;
|
||
registerModal.value.add();
|
||
}
|
||
|
||
function handleEdit(record: Recordable) {
|
||
registerModal.value.disableSubmit = false;
|
||
registerModal.value.edit(record);
|
||
}
|
||
|
||
function handleDetail(record: Recordable) {
|
||
registerModal.value.disableSubmit = true;
|
||
registerModal.value.edit(record);
|
||
}
|
||
|
||
async function handleDelete(record) {
|
||
await deleteOne({ id: record.id }, handleSuccess);
|
||
}
|
||
|
||
async function batchHandleDelete() {
|
||
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
|
||
}
|
||
|
||
function handleSuccess() {
|
||
(selectedRowKeys.value = []) && reload();
|
||
}
|
||
|
||
function getTableAction(record) {
|
||
return [
|
||
{
|
||
label: '编辑',
|
||
onClick: handleEdit.bind(null, record),
|
||
auth: '{{entityPackage}}:{{tableName}}:edit',
|
||
},
|
||
];
|
||
}
|
||
|
||
function getDropDownAction(record) {
|
||
return [
|
||
{
|
||
label: '详情',
|
||
onClick: handleDetail.bind(null, record),
|
||
},
|
||
{
|
||
label: '删除',
|
||
popConfirm: {
|
||
title: '是否确认删除',
|
||
confirm: handleDelete.bind(null, record),
|
||
placement: 'topLeft',
|
||
},
|
||
auth: '{{entityPackage}}:{{tableName}}:delete',
|
||
},
|
||
];
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
:deep(.ant-picker-range) {
|
||
width: 100%;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
### A10N. Modal组件 - vue3Native 原生风格
|
||
|
||
```vue
|
||
<template>
|
||
<j-modal :title="title" :width="800" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">
|
||
<{{entityName}}Form ref="registerForm" @ok="submitCallback" :formDisabled="disableSubmit" :formBpm="false" />
|
||
<template #footer>
|
||
<a-button @click="handleCancel">取消</a-button>
|
||
<a-button :class="{ 'jee-hidden': disableSubmit }" type="primary" @click="handleOk">确认</a-button>
|
||
</template>
|
||
</j-modal>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, nextTick, defineExpose } from 'vue';
|
||
import {{entityName}}Form from './{{entityName}}Form.vue';
|
||
import JModal from '/@/components/Modal/src/JModal/JModal.vue';
|
||
|
||
const title = ref<string>('');
|
||
const visible = ref<boolean>(false);
|
||
const disableSubmit = ref<boolean>(false);
|
||
const registerForm = ref();
|
||
const emit = defineEmits(['register', 'success']);
|
||
|
||
/**
|
||
* 新增
|
||
*/
|
||
function add() {
|
||
title.value = '新增';
|
||
visible.value = true;
|
||
nextTick(() => {
|
||
registerForm.value.add();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 编辑
|
||
*/
|
||
function edit(record) {
|
||
title.value = disableSubmit.value ? '详情' : '编辑';
|
||
visible.value = true;
|
||
nextTick(() => {
|
||
registerForm.value.edit(record);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 确定按钮点击事件
|
||
*/
|
||
function handleOk() {
|
||
registerForm.value.submitForm();
|
||
}
|
||
|
||
/**
|
||
* form保存回调事件
|
||
*/
|
||
function submitCallback() {
|
||
handleCancel();
|
||
emit('success');
|
||
}
|
||
|
||
/**
|
||
* 取消按钮回调事件
|
||
*/
|
||
function handleCancel() {
|
||
visible.value = false;
|
||
}
|
||
|
||
defineExpose({
|
||
add,
|
||
edit,
|
||
disableSubmit,
|
||
});
|
||
</script>
|
||
|
||
<style lang="less">
|
||
.jee-hidden {
|
||
display: none !important;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
### A11N. Form组件 - vue3Native 原生风格
|
||
|
||
```vue
|
||
<template>
|
||
<a-spin :spinning="confirmLoading">
|
||
<JFormContainer :disabled="disabled">
|
||
<template #detail>
|
||
<a-form ref="formRef" class="antd-modal-form" :labelCol="labelCol" :wrapperCol="wrapperCol" name="{{entityName}}Form">
|
||
<a-row>
|
||
<!-- 每个字段生成一个 a-col + a-form-item -->
|
||
|
||
<!-- === 文本输入 === -->
|
||
<!-- <a-col :span="24">
|
||
<a-form-item label="名称" v-bind="validateInfos.name" id="{{entityName}}Form-name" name="name">
|
||
<a-input v-model:value="formData.name" placeholder="请输入名称" allow-clear />
|
||
</a-form-item>
|
||
</a-col> -->
|
||
|
||
<!-- === 数字输入 === -->
|
||
<!-- <a-col :span="24">
|
||
<a-form-item label="数量" v-bind="validateInfos.quantity" id="{{entityName}}Form-quantity" name="quantity">
|
||
<a-input-number v-model:value="formData.quantity" placeholder="请输入数量" style="width: 100%" />
|
||
</a-form-item>
|
||
</a-col> -->
|
||
|
||
<!-- === 字典下拉 === -->
|
||
<!-- <a-col :span="24">
|
||
<a-form-item label="状态" v-bind="validateInfos.status" id="{{entityName}}Form-status" name="status">
|
||
<JDictSelectTag v-model:value="formData.status" dictCode="dict_code" placeholder="请选择状态" />
|
||
</a-form-item>
|
||
</a-col> -->
|
||
|
||
<!-- === Switch === -->
|
||
<!-- <a-col :span="24">
|
||
<a-form-item label="是否启用" v-bind="validateInfos.enabled" id="{{entityName}}Form-enabled" name="enabled">
|
||
<a-switch v-model:checked="formData.enabled" checkedValue="Y" unCheckedValue="N" />
|
||
</a-form-item>
|
||
</a-col> -->
|
||
|
||
<!-- === 日期选择 === -->
|
||
<!-- <a-col :span="24">
|
||
<a-form-item label="日期" v-bind="validateInfos.dateField" id="{{entityName}}Form-dateField" name="dateField">
|
||
<a-date-picker v-model:value="formData.dateField" placeholder="请选择日期" value-format="YYYY-MM-DD" style="width: 100%" />
|
||
</a-form-item>
|
||
</a-col> -->
|
||
|
||
<!-- === 日期时间选择 === -->
|
||
<!-- <a-col :span="24">
|
||
<a-form-item label="日期时间" v-bind="validateInfos.datetimeField" id="{{entityName}}Form-datetimeField" name="datetimeField">
|
||
<a-date-picker v-model:value="formData.datetimeField" placeholder="请选择日期时间" :showTime="true" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
|
||
</a-form-item>
|
||
</a-col> -->
|
||
|
||
<!-- === 文本域 === -->
|
||
<!-- <a-col :span="24">
|
||
<a-form-item label="备注" v-bind="validateInfos.remark" id="{{entityName}}Form-remark" name="remark">
|
||
<a-textarea v-model:value="formData.remark" placeholder="请输入备注" :rows="4" />
|
||
</a-form-item>
|
||
</a-col> -->
|
||
|
||
<!-- === 图片上传 === -->
|
||
<!-- <a-col :span="24">
|
||
<a-form-item label="图片" v-bind="validateInfos.imageField" id="{{entityName}}Form-imageField" name="imageField">
|
||
<JImageUpload v-model:value="formData.imageField" />
|
||
</a-form-item>
|
||
</a-col> -->
|
||
|
||
<!-- === 文件上传 === -->
|
||
<!-- <a-col :span="24">
|
||
<a-form-item label="附件" v-bind="validateInfos.fileField" id="{{entityName}}Form-fileField" name="fileField">
|
||
<JUpload v-model:value="formData.fileField" />
|
||
</a-form-item>
|
||
</a-col> -->
|
||
|
||
<!-- === 富文本 === -->
|
||
<!-- <a-col :span="24">
|
||
<a-form-item label="内容" v-bind="validateInfos.content" id="{{entityName}}Form-content" name="content">
|
||
<JEditor v-model:value="formData.content" />
|
||
</a-form-item>
|
||
</a-col> -->
|
||
|
||
<!-- === 用户选择 === -->
|
||
<!-- <a-col :span="24">
|
||
<a-form-item label="负责人" v-bind="validateInfos.userId" id="{{entityName}}Form-userId" name="userId">
|
||
<JSelectUserByDept v-model:value="formData.userId" />
|
||
</a-form-item>
|
||
</a-col> -->
|
||
|
||
<!-- === 部门选择 === -->
|
||
<!-- <a-col :span="24">
|
||
<a-form-item label="部门" v-bind="validateInfos.deptId" id="{{entityName}}Form-deptId" name="deptId">
|
||
<JSelectDept v-model:value="formData.deptId" />
|
||
</a-form-item>
|
||
</a-col> -->
|
||
|
||
<!-- === 搜索选择 === -->
|
||
<!-- <a-col :span="24">
|
||
<a-form-item label="搜索" v-bind="validateInfos.searchField" id="{{entityName}}Form-searchField" name="searchField">
|
||
<JSearchSelect v-model:value="formData.searchField" dict="tableName,textField,codeField" placeholder="请选择" />
|
||
</a-form-item>
|
||
</a-col> -->
|
||
</a-row>
|
||
</a-form>
|
||
</template>
|
||
</JFormContainer>
|
||
</a-spin>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, reactive, defineExpose, nextTick, defineProps, computed } from 'vue';
|
||
import { useMessage } from '/@/hooks/web/useMessage';
|
||
import { getValueType } from '/@/utils';
|
||
import { saveOrUpdate } from '../{{entityName}}.api';
|
||
import { Form } from 'ant-design-vue';
|
||
import JFormContainer from '/@/components/Form/src/container/JFormContainer.vue';
|
||
// 按需导入组件(根据实际用到的字段类型)
|
||
// import JDictSelectTag from '/@/components/Form/src/jeecg/components/JDictSelectTag.vue';
|
||
// import JSearchSelect from '/@/components/Form/src/jeecg/components/JSearchSelect.vue';
|
||
// import JImageUpload from '/@/components/Form/src/jeecg/components/JImageUpload.vue';
|
||
// import JUpload from '/@/components/Form/src/jeecg/components/JUpload.vue';
|
||
// import JEditor from '/@/components/Form/src/jeecg/components/JEditor.vue';
|
||
// import JSelectUserByDept from '/@/components/Form/src/jeecg/components/JSelectUserByDept.vue';
|
||
// import JSelectDept from '/@/components/Form/src/jeecg/components/JSelectDept.vue';
|
||
|
||
const props = defineProps({
|
||
formDisabled: { type: Boolean, default: false },
|
||
formData: { type: Object, default: () => ({}) },
|
||
formBpm: { type: Boolean, default: true },
|
||
});
|
||
const formRef = ref();
|
||
const useForm = Form.useForm;
|
||
const emit = defineEmits(['register', 'ok']);
|
||
const formData = reactive<Record<string, any>>({
|
||
id: '',
|
||
// 业务字段初始值
|
||
// name: '',
|
||
// status: '',
|
||
// ... 所有字段默认值
|
||
});
|
||
const { createMessage } = useMessage();
|
||
const labelCol = ref<any>({ xs: { span: 24 }, sm: { span: 5 } });
|
||
const wrapperCol = ref<any>({ xs: { span: 24 }, sm: { span: 16 } });
|
||
const confirmLoading = ref<boolean>(false);
|
||
|
||
// 表单验证规则
|
||
const validatorRules = reactive({
|
||
// name: [{ required: true, message: '请输入名称!' }],
|
||
// ... 必填字段的验证规则
|
||
});
|
||
const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: false });
|
||
|
||
// 表单禁用
|
||
const disabled = computed(() => {
|
||
if (props.formBpm === true) {
|
||
if (props.formData.disabled === false) {
|
||
return false;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
return props.formDisabled;
|
||
});
|
||
|
||
/**
|
||
* 新增
|
||
*/
|
||
function add() {
|
||
edit({});
|
||
}
|
||
|
||
/**
|
||
* 编辑
|
||
*/
|
||
function edit(record) {
|
||
nextTick(() => {
|
||
resetFields();
|
||
const tmpData = {};
|
||
Object.keys(formData).forEach((key) => {
|
||
if (record.hasOwnProperty(key)) {
|
||
tmpData[key] = record[key];
|
||
}
|
||
});
|
||
// 赋值
|
||
Object.assign(formData, tmpData);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 提交数据
|
||
*/
|
||
async function submitForm() {
|
||
try {
|
||
await validate();
|
||
} catch ({ errorFields }) {
|
||
if (errorFields) {
|
||
const firstField = errorFields[0];
|
||
if (firstField) {
|
||
formRef.value.scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
|
||
}
|
||
}
|
||
return Promise.reject(errorFields);
|
||
}
|
||
confirmLoading.value = true;
|
||
const isUpdate = ref<boolean>(false);
|
||
let model = formData;
|
||
if (model.id) {
|
||
isUpdate.value = true;
|
||
}
|
||
// 处理数组类型字段(多选等)
|
||
for (let data in model) {
|
||
if (model[data] instanceof Array) {
|
||
let valueType = getValueType(formRef.value.getProps, data);
|
||
if (valueType === 'string') {
|
||
model[data] = model[data].join(',');
|
||
}
|
||
}
|
||
}
|
||
await saveOrUpdate(model, isUpdate.value)
|
||
.then((res) => {
|
||
if (res.success) {
|
||
createMessage.success(res.message);
|
||
emit('ok');
|
||
} else {
|
||
createMessage.warning(res.message);
|
||
}
|
||
})
|
||
.finally(() => {
|
||
confirmLoading.value = false;
|
||
});
|
||
}
|
||
|
||
defineExpose({
|
||
add,
|
||
edit,
|
||
submitForm,
|
||
});
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.antd-modal-form {
|
||
padding: 14px;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
**vue3Native 多列布局:**
|
||
- 单列: `<a-col :span="24">`
|
||
- 双列: `<a-col :span="12">`
|
||
- 三列: `<a-col :span="8">`
|
||
|
||
---
|
||
|
||
### A12. 菜单权限 SQL
|
||
|
||
```sql
|
||
-- 注意:该页面对应的前台目录为 views/{{viewDir}} 文件夹下
|
||
-- 如果你想更改到其他目录,请修改sql中component字段对应的值
|
||
|
||
-- 主菜单
|
||
INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external)
|
||
VALUES ('{{timestamp}}01', NULL, '{{description}}', '/{{entityPackagePath}}/{{entityName_uncap}}List', '{{viewDir}}/{{entityName}}List', NULL, NULL, 0, NULL, '1', 0.00, 0, NULL, 1, 0, 0, 0, 0, NULL, '1', 0, 0, 'admin', '{{today}} 00:00:00', NULL, NULL, 0);
|
||
|
||
-- 新增
|
||
INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||
VALUES ('{{timestamp}}02', '{{timestamp}}01', '添加{{description}}', NULL, NULL, 0, NULL, NULL, 2, '{{entityPackage}}:{{tableName}}:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '{{today}} 00:00:00', NULL, NULL, 0, 0, '1', 0);
|
||
|
||
-- 编辑
|
||
INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||
VALUES ('{{timestamp}}03', '{{timestamp}}01', '编辑{{description}}', NULL, NULL, 0, NULL, NULL, 2, '{{entityPackage}}:{{tableName}}:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '{{today}} 00:00:00', NULL, NULL, 0, 0, '1', 0);
|
||
|
||
-- 删除
|
||
INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||
VALUES ('{{timestamp}}04', '{{timestamp}}01', '删除{{description}}', NULL, NULL, 0, NULL, NULL, 2, '{{entityPackage}}:{{tableName}}:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '{{today}} 00:00:00', NULL, NULL, 0, 0, '1', 0);
|
||
|
||
-- 批量删除
|
||
INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||
VALUES ('{{timestamp}}05', '{{timestamp}}01', '批量删除{{description}}', NULL, NULL, 0, NULL, NULL, 2, '{{entityPackage}}:{{tableName}}:deleteBatch', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '{{today}} 00:00:00', NULL, NULL, 0, 0, '1', 0);
|
||
|
||
-- 导出excel
|
||
INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||
VALUES ('{{timestamp}}06', '{{timestamp}}01', '导出excel_{{description}}', NULL, NULL, 0, NULL, NULL, 2, '{{entityPackage}}:{{tableName}}:exportXls', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '{{today}} 00:00:00', NULL, NULL, 0, 0, '1', 0);
|
||
|
||
-- 导入excel
|
||
INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||
VALUES ('{{timestamp}}07', '{{timestamp}}01', '导入excel_{{description}}', NULL, NULL, 0, NULL, NULL, 2, '{{entityPackage}}:{{tableName}}:importExcel', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '{{today}} 00:00:00', NULL, NULL, 0, 0, '1', 0);
|
||
|
||
-- 角色授权(admin角色)
|
||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('{{timestamp}}08', 'f6817f48af4fb3af11b9e8bf182f618b', '{{timestamp}}01', NULL, '{{today}} 00:00:00', '127.0.0.1');
|
||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('{{timestamp}}09', 'f6817f48af4fb3af11b9e8bf182f618b', '{{timestamp}}02', NULL, '{{today}} 00:00:00', '127.0.0.1');
|
||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('{{timestamp}}10', 'f6817f48af4fb3af11b9e8bf182f618b', '{{timestamp}}03', NULL, '{{today}} 00:00:00', '127.0.0.1');
|
||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('{{timestamp}}11', 'f6817f48af4fb3af11b9e8bf182f618b', '{{timestamp}}04', NULL, '{{today}} 00:00:00', '127.0.0.1');
|
||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('{{timestamp}}12', 'f6817f48af4fb3af11b9e8bf182f618b', '{{timestamp}}05', NULL, '{{today}} 00:00:00', '127.0.0.1');
|
||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('{{timestamp}}13', 'f6817f48af4fb3af11b9e8bf182f618b', '{{timestamp}}06', NULL, '{{today}} 00:00:00', '127.0.0.1');
|
||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('{{timestamp}}14', 'f6817f48af4fb3af11b9e8bf182f618b', '{{timestamp}}07', NULL, '{{today}} 00:00:00', '127.0.0.1');
|
||
```
|
||
|
||
---
|
||
|
||
## B. 树表模式差异
|
||
|
||
树表在单表基础上有以下差异:
|
||
|
||
### B1. Entity 额外字段
|
||
|
||
```java
|
||
/**父级节点*/
|
||
@Excel(name = "父级节点", width = 15)
|
||
@Schema(description = "父级节点")
|
||
private String pid;
|
||
/**是否有子节点*/
|
||
@Excel(name = "是否有子节点", width = 15, dicCode = "yn")
|
||
@Dict(dicCode = "yn")
|
||
@TableField(value = "has_child")
|
||
@Schema(description = "是否有子节点")
|
||
private String hasChild;
|
||
```
|
||
|
||
### B2. Mapper 额外方法
|
||
|
||
```java
|
||
public interface {{entityName}}Mapper extends BaseMapper<{{entityName}}> {
|
||
|
||
/**
|
||
* 编辑节点状态
|
||
*/
|
||
@Update("update {{tableName}} set has_child=#{hasChild} where id = #{pid}")
|
||
void updateTreeNodeStatus(@Param("pid") String pid, @Param("hasChild") String hasChild);
|
||
|
||
/**
|
||
* 根据pid查询子节点
|
||
*/
|
||
@Select("select * from {{tableName}} where pid = #{parentId} ${installCondition}")
|
||
List<{{entityName}}> queryListByPid(@Param("parentId") String parentId, @Param("installCondition") String installCondition);
|
||
}
|
||
```
|
||
|
||
### B3. Service 接口额外方法
|
||
|
||
```java
|
||
public interface I{{entityName}}Service extends IService<{{entityName}}> {
|
||
public static final String ROOT_PID_VALUE = "0";
|
||
public static final String HASCHILD = "1";
|
||
public static final String NOCHILD = "0";
|
||
|
||
void add{{entityName}}({{entityName}} entity);
|
||
void update{{entityName}}({{entityName}} entity);
|
||
void delete{{entityName}}(String id) throws JeecgBootException;
|
||
List<{{entityName}}> queryTreeListNoPage(QueryWrapper<{{entityName}}> queryWrapper);
|
||
List<{{entityName}}> queryListByCode(String code);
|
||
List<{{entityName}}> queryListByPid(String pid);
|
||
}
|
||
```
|
||
|
||
### B4. ServiceImpl 核心逻辑
|
||
|
||
```java
|
||
@Service
|
||
public class {{entityName}}ServiceImpl extends ServiceImpl<{{entityName}}Mapper, {{entityName}}> implements I{{entityName}}Service {
|
||
|
||
@Override
|
||
public void add{{entityName}}({{entityName}} entity) {
|
||
if (oConvertUtils.isEmpty(entity.getPid())) {
|
||
entity.setPid(I{{entityName}}Service.ROOT_PID_VALUE);
|
||
} else {
|
||
// 如果当前节点父ID不为空 则设置父节点的hasChild为1
|
||
{{entityName}} parent = baseMapper.selectById(entity.getPid());
|
||
if (parent != null && !I{{entityName}}Service.HASCHILD.equals(parent.getHasChild())) {
|
||
parent.setHasChild(I{{entityName}}Service.HASCHILD);
|
||
baseMapper.updateById(parent);
|
||
}
|
||
}
|
||
baseMapper.insert(entity);
|
||
}
|
||
|
||
@Override
|
||
public void update{{entityName}}({{entityName}} entity) {
|
||
{{entityName}} old = baseMapper.selectById(entity.getId());
|
||
if (old != null && !old.getPid().equals(entity.getPid())) {
|
||
// 更新新父节点状态
|
||
updateOldParentNode(entity.getPid());
|
||
// 更新旧父节点状态(检查是否还有子节点)
|
||
int childCount = baseMapper.selectCount(new QueryWrapper<{{entityName}}>().eq("pid", old.getPid())).intValue();
|
||
if (childCount == 1) {
|
||
// 当前是唯一子节点,更新旧父为无子节点
|
||
if (!I{{entityName}}Service.ROOT_PID_VALUE.equals(old.getPid())) {
|
||
baseMapper.updateTreeNodeStatus(old.getPid(), I{{entityName}}Service.NOCHILD);
|
||
}
|
||
}
|
||
}
|
||
baseMapper.updateById(entity);
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public void delete{{entityName}}(String id) throws JeecgBootException {
|
||
// 查询选中节点下所有子节点一起删除
|
||
id = TreeUtils.getTreeChildIds(id, (pid) -> baseMapper.queryListByPid(pid, null));
|
||
if (id.indexOf(",") > 0) {
|
||
baseMapper.deleteBatchIds(Arrays.asList(id.split(",")));
|
||
} else {
|
||
baseMapper.deleteById(id);
|
||
}
|
||
// 更新父节点状态
|
||
{{entityName}} entity = baseMapper.selectById(id.split(",")[0]);
|
||
if (entity != null) {
|
||
updateOldParentNode(entity.getPid());
|
||
}
|
||
}
|
||
|
||
private void updateOldParentNode(String pid) {
|
||
if (!I{{entityName}}Service.ROOT_PID_VALUE.equals(pid)) {
|
||
{{entityName}} parent = baseMapper.selectById(pid);
|
||
if (parent != null) {
|
||
parent.setHasChild(I{{entityName}}Service.HASCHILD);
|
||
baseMapper.updateById(parent);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### B5. Controller 额外端点
|
||
|
||
```java
|
||
/**
|
||
* 查询根节点数据
|
||
*/
|
||
@GetMapping(value = "/rootList")
|
||
public Result<IPage<{{entityName}}>> rootList({{entityName}} entity,
|
||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||
HttpServletRequest req) {
|
||
// 判断有没有查询条件,如果有则按条件查全部(不分树层级)
|
||
String hasQuery = req.getParameter("hasQuery");
|
||
if (oConvertUtils.isNotEmpty(hasQuery) && "true".equals(hasQuery)) {
|
||
QueryWrapper<{{entityName}}> queryWrapper = QueryGenerator.initQueryWrapper(entity, req.getParameterMap());
|
||
Page<{{entityName}}> page = new Page<>(pageNo, pageSize);
|
||
IPage<{{entityName}}> pageList = {{entityName_uncap}}Service.page(page, queryWrapper);
|
||
return Result.OK(pageList);
|
||
}
|
||
// 无查询条件则查根节点
|
||
QueryWrapper<{{entityName}}> queryWrapper = QueryGenerator.initQueryWrapper(entity, req.getParameterMap());
|
||
queryWrapper.eq("pid", I{{entityName}}Service.ROOT_PID_VALUE);
|
||
Page<{{entityName}}> page = new Page<>(pageNo, pageSize);
|
||
IPage<{{entityName}}> pageList = {{entityName_uncap}}Service.page(page, queryWrapper);
|
||
return Result.OK(pageList);
|
||
}
|
||
|
||
/**
|
||
* 查询子节点
|
||
*/
|
||
@GetMapping(value = "/childList")
|
||
public Result<List<{{entityName}}>> childList({{entityName}} entity,
|
||
HttpServletRequest req) {
|
||
QueryWrapper<{{entityName}}> queryWrapper = QueryGenerator.initQueryWrapper(entity, req.getParameterMap());
|
||
List<{{entityName}}> list = {{entityName_uncap}}Service.list(queryWrapper);
|
||
return Result.OK(list);
|
||
}
|
||
|
||
/**
|
||
* 批量查询子节点
|
||
*/
|
||
@GetMapping(value = "/getChildListBatch")
|
||
public Result getChildListBatch(@RequestParam("parentIds") String parentIds) {
|
||
List<{{entityName}}> list = {{entityName_uncap}}Service.queryListByPid(parentIds);
|
||
IPage<{{entityName}}> pageList = new Page<>(1, 10, list.size());
|
||
pageList.setRecords(list);
|
||
return Result.OK(pageList);
|
||
}
|
||
```
|
||
|
||
### B6. 树表前端 API 额外方法
|
||
|
||
```typescript
|
||
enum Api {
|
||
list = '/{{entityPackagePath}}/{{entityName_uncap}}/rootList', // 注意是 rootList
|
||
// ... 其他同单表
|
||
loadTreeData = '/{{entityPackagePath}}/{{entityName_uncap}}/rootList',
|
||
getChildList = '/{{entityPackagePath}}/{{entityName_uncap}}/childList',
|
||
getChildListBatch = '/{{entityPackagePath}}/{{entityName_uncap}}/getChildListBatch',
|
||
}
|
||
|
||
// 额外导出
|
||
export const loadTreeData = (params) => defHttp.get({ url: Api.loadTreeData, params });
|
||
export const getChildList = (params) => defHttp.get({ url: Api.getChildList, params });
|
||
export const getChildListBatch = (params) => defHttp.get({ url: Api.getChildListBatch, params }, { isTransformResponse: false });
|
||
```
|
||
|
||
### B7. 树表前端 List 页面差异
|
||
|
||
vue3 封装风格中 `useListPage` 的 `tableProps` 增加:
|
||
```typescript
|
||
tableProps: {
|
||
// ... 同单表
|
||
isTreeTable: true, // 标记为树表
|
||
// list API 改为 rootList
|
||
}
|
||
```
|
||
|
||
模板中增加展开/加载子节点逻辑(参考 `src/views/system/category/index.vue`)。
|
||
|
||
---
|
||
|
||
## C. 一对多模式差异
|
||
|
||
一对多在单表基础上有以下差异:
|
||
|
||
### C1. 子表 Entity
|
||
|
||
每个子表生成独立的 Entity,包含外键字段:
|
||
```java
|
||
/**主表ID(外键)*/
|
||
@Schema(description = "主表ID")
|
||
private String {{mainEntityName_uncap}}Id; // 外键字段名默认为 主表实体名(camelCase) + Id
|
||
```
|
||
注意:子表 Entity 的外键字段不加 `@Excel` 注解(导出时忽略)。
|
||
|
||
### C2. 子表 Mapper
|
||
|
||
```java
|
||
public interface {{subEntityName}}Mapper extends BaseMapper<{{subEntityName}}> {
|
||
/**
|
||
* 通过主表id删除子表数据
|
||
*/
|
||
@Delete("DELETE FROM {{subTableName}} WHERE {{foreignKey}} = #{mainId}")
|
||
boolean deleteByMainId(@Param("mainId") String mainId);
|
||
|
||
/**
|
||
* 通过主表id查询子表数据
|
||
*/
|
||
List<{{subEntityName}}> selectByMainId(@Param("mainId") String mainId);
|
||
}
|
||
```
|
||
|
||
### C3. 主表 Service 接口
|
||
|
||
```java
|
||
public interface I{{entityName}}Service extends IService<{{entityName}}> {
|
||
/**
|
||
* 添加一对多
|
||
*/
|
||
public void saveMain({{entityName}} entity, List<{{subEntityName}}> subList);
|
||
|
||
/**
|
||
* 修改一对多
|
||
*/
|
||
public void updateMain({{entityName}} entity, List<{{subEntityName}}> subList);
|
||
|
||
/**
|
||
* 删除一对多
|
||
*/
|
||
public void delMain(String id);
|
||
|
||
/**
|
||
* 批量删除一对多
|
||
*/
|
||
public void delBatchMain(Collection<? extends Serializable> idList);
|
||
}
|
||
```
|
||
|
||
### C4. 主表 ServiceImpl
|
||
|
||
```java
|
||
@Service
|
||
public class {{entityName}}ServiceImpl extends ServiceImpl<{{entityName}}Mapper, {{entityName}}> implements I{{entityName}}Service {
|
||
|
||
@Autowired
|
||
private {{subEntityName}}Mapper {{subEntityName_uncap}}Mapper;
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public void saveMain({{entityName}} entity, List<{{subEntityName}}> subList) {
|
||
baseMapper.insert(entity);
|
||
if (subList != null && subList.size() > 0) {
|
||
for ({{subEntityName}} sub : subList) {
|
||
sub.set{{mainEntityName}}Id(entity.getId()); // 设置外键
|
||
{{subEntityName_uncap}}Mapper.insert(sub);
|
||
}
|
||
}
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public void updateMain({{entityName}} entity, List<{{subEntityName}}> subList) {
|
||
baseMapper.updateById(entity);
|
||
// 先删后增
|
||
{{subEntityName_uncap}}Mapper.deleteByMainId(entity.getId());
|
||
if (subList != null && subList.size() > 0) {
|
||
for ({{subEntityName}} sub : subList) {
|
||
sub.set{{mainEntityName}}Id(entity.getId());
|
||
{{subEntityName_uncap}}Mapper.insert(sub);
|
||
}
|
||
}
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public void delMain(String id) {
|
||
{{subEntityName_uncap}}Mapper.deleteByMainId(id);
|
||
baseMapper.deleteById(id);
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public void delBatchMain(Collection<? extends Serializable> idList) {
|
||
for (Serializable id : idList) {
|
||
{{subEntityName_uncap}}Mapper.deleteByMainId(id.toString());
|
||
baseMapper.deleteById(id);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### C5. Page VO(Excel导入导出用)
|
||
|
||
```java
|
||
package org.jeecg.modules.{{entityPackage}}.vo;
|
||
|
||
import org.jeecg.modules.{{entityPackage}}.entity.{{entityName}};
|
||
import org.jeecg.modules.{{entityPackage}}.entity.{{subEntityName}};
|
||
import lombok.Data;
|
||
import org.jeecgframework.poi.excel.annotation.ExcelCollection;
|
||
import java.util.List;
|
||
|
||
@Data
|
||
public class {{entityName}}Page {
|
||
// 主表字段(同 Entity,不含系统字段)
|
||
// ...
|
||
|
||
@ExcelCollection(name = "{{subDescription}}")
|
||
private List<{{subEntityName}}> {{subEntityName_uncap}}List;
|
||
}
|
||
```
|
||
|
||
### C6. Controller 额外端点
|
||
|
||
```java
|
||
/**
|
||
* 查询子表数据
|
||
*/
|
||
@GetMapping(value = "/query{{subEntityName}}ByMainId")
|
||
public Result<List<{{subEntityName}}>> query{{subEntityName}}ByMainId(@RequestParam(name = "id", required = true) String id) {
|
||
List<{{subEntityName}}> list = {{subEntityName_uncap}}Mapper.selectByMainId(id);
|
||
return Result.OK(list);
|
||
}
|
||
```
|
||
|
||
### C7. 前端差异
|
||
|
||
主要差异在于编辑 Modal/Form 中包含子表的 Tab 页或内嵌表格:
|
||
- 主表字段 + 子表 Tab(使用 `a-tabs` 切换)
|
||
- 子表使用 `JEditableTable` 或 `BasicTable` 展示和编辑
|
||
- 保存时收集主表 + 子表数据一起提交
|
||
|
||
---
|
||
|
||
## D. 字段类型完整映射速查表
|
||
|
||
| 业务语义 | DB列类型 | Java类型 | @Excel format | vue3 FormSchema component | vue3Native 控件 | 查询组件 |
|
||
|----------|---------|----------|---------------|--------------------------|----------------|---------|
|
||
| 名称/编码/标题 | varchar(100) | String | - | Input | a-input | JInput |
|
||
| 金额/价格 | decimal(10,2) | BigDecimal | - | InputNumber | a-input-number | InputNumber |
|
||
| 整数/数量 | int(11) | Integer | - | InputNumber | a-input-number | InputNumber |
|
||
| 浮点数 | double | Double | - | InputNumber | a-input-number | InputNumber |
|
||
| 状态/类型(字典) | varchar(10) | String | dicCode | JDictSelectTag | JDictSelectTag | JDictSelectTag |
|
||
| 单选(字典) | varchar(10) | String | dicCode | JDictSelectTag(type=radio) | a-radio-group | JDictSelectTag |
|
||
| 多选(字典) | varchar(200) | String | dicCode | JDictSelectTag(type=checkbox) | a-checkbox-group | - |
|
||
| 开关/是否 | varchar(2) | String | - | JSwitch | a-switch | - |
|
||
| 日期 | date | Date | yyyy-MM-dd | DatePicker | a-date-picker | DatePicker |
|
||
| 日期时间 | datetime | Date | yyyy-MM-dd HH:mm:ss | DatePicker(showTime) | a-date-picker(showTime) | DatePicker(showTime) |
|
||
| 长文本/备注 | text | String | - | InputTextArea | a-textarea | - |
|
||
| 富文本 | text | String | - | JEditor | JEditor | - |
|
||
| Markdown | text | String | - | JMarkdownEditor | JMarkdownEditor | - |
|
||
| 图片 | varchar(1000) | String | - | JImageUpload | JImageUpload | - |
|
||
| 文件/附件 | varchar(1000) | String | - | JUpload | JUpload | - |
|
||
| 用户选择 | varchar(32) | String | dictTable=sys_user | JSelectUserByDept | JSelectUserByDept | - |
|
||
| 部门选择 | varchar(32) | String | dictTable=sys_depart | JSelectDept | JSelectDept | - |
|
||
| 分类树 | varchar(64) | String | - | JCategorySelect | JCategorySelect | JCategorySelect |
|
||
| 搜索选择 | varchar(32) | String | dictTable | JSearchSelect | JSearchSelect | JSearchSelect |
|
||
| 省市区 | varchar(200) | String | - | JAreaLinkage | JAreaLinkage | - |
|
||
| 排序号 | int(11) | Integer | - | InputNumber | a-input-number | - |
|
||
|
||
## E. 建表 DDL 模板(如需要自动建表)
|
||
|
||
```sql
|
||
CREATE TABLE `{{tableName}}` (
|
||
`id` varchar(36) NOT NULL COMMENT '主键',
|
||
-- 业务字段
|
||
-- `field_name` varchar(100) DEFAULT NULL COMMENT '字段注释',
|
||
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||
`create_time` datetime DEFAULT NULL COMMENT '创建日期',
|
||
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||
`update_time` datetime DEFAULT NULL COMMENT '更新日期',
|
||
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '所属部门',
|
||
PRIMARY KEY (`id`) USING BTREE
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='{{description}}';
|
||
```
|
||
|
||
树表额外字段:
|
||
```sql
|
||
`pid` varchar(36) DEFAULT NULL COMMENT '父级节点',
|
||
`has_child` varchar(3) DEFAULT NULL COMMENT '是否有子节点',
|
||
```
|
||
|
||
子表额外字段:
|
||
```sql
|
||
`{{main_table_name}}_id` varchar(36) DEFAULT NULL COMMENT '主表外键',
|
||
```
|
||
|
||
---
|
||
|
||
## F. 增量字段修改(加字段/删字段/改字段)
|
||
|
||
### F1. 定位已有代码文件
|
||
|
||
增量修改时,必须先找到并读取所有相关文件:
|
||
|
||
```
|
||
后端文件(在后端根目录搜索):
|
||
- **/entity/{EntityName}.java → 实体类
|
||
- **/controller/{EntityName}Controller.java → 控制器(通常不需要改)
|
||
- **/service/I{EntityName}Service.java → Service接口(通常不需要改)
|
||
- **/service/impl/{EntityName}ServiceImpl.java → Service实现(通常不需要改)
|
||
|
||
前端文件(在前端 src/views/ 下搜索):
|
||
- **/{EntityName}.data.ts → 列定义 + 表单Schema
|
||
- **/{EntityName}List.vue → 列表页(通常不需要改)
|
||
- **/{EntityName}Modal.vue → 弹窗(通常不需要改)
|
||
- **/{EntityName}Form.vue → 表单(vue3Native风格,需要改)
|
||
```
|
||
|
||
### F2. 加字段 — 需要修改的位置
|
||
|
||
**每加一个字段,需要修改以下文件:**
|
||
|
||
#### 1) Entity.java — 在业务字段区域末尾追加
|
||
|
||
根据字段类型选择对应的注解模式(参考 A1 节的业务字段模板):
|
||
|
||
```java
|
||
// String 字段
|
||
@Excel(name = "字段注释", width = 15)
|
||
@Schema(description = "字段注释")
|
||
private String fieldName;
|
||
|
||
// 带字典的 String 字段
|
||
@Excel(name = "字段注释", width = 15, dicCode = "dict_code")
|
||
@Dict(dicCode = "dict_code")
|
||
@Schema(description = "字段注释")
|
||
private String fieldName;
|
||
|
||
// Integer 字段
|
||
@Excel(name = "字段注释", width = 15)
|
||
@Schema(description = "字段注释")
|
||
private Integer fieldName;
|
||
|
||
// BigDecimal 字段(需确认 import java.math.BigDecimal 已存在)
|
||
@Excel(name = "字段注释", width = 15)
|
||
@Schema(description = "字段注释")
|
||
private BigDecimal fieldName;
|
||
|
||
// Date 字段(需确认 import java.util.Date + JsonFormat + DateTimeFormat 已导入)
|
||
@Excel(name = "字段注释", width = 15, format = "yyyy-MM-dd")
|
||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
|
||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||
@Schema(description = "字段注释")
|
||
private Date fieldName;
|
||
|
||
// DateTime 字段
|
||
@Excel(name = "字段注释", width = 20, format = "yyyy-MM-dd HH:mm:ss")
|
||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||
@Schema(description = "字段注释")
|
||
private Date fieldName;
|
||
```
|
||
|
||
**注意:** 检查是否需要新增 import 语句(如 BigDecimal、Date、JsonFormat、Dict 等)。
|
||
|
||
#### 2) *.data.ts — 三处追加
|
||
|
||
**a) columns 数组末尾追加列定义:**
|
||
```typescript
|
||
// 普通列
|
||
{
|
||
title: '字段名称',
|
||
align: 'center',
|
||
dataIndex: 'fieldName',
|
||
},
|
||
// 字典列(dataIndex 加 _dictText 后缀)
|
||
{
|
||
title: '状态',
|
||
align: 'center',
|
||
dataIndex: 'status_dictText',
|
||
},
|
||
// 图片列
|
||
{
|
||
title: '图片',
|
||
align: 'center',
|
||
dataIndex: 'imageField',
|
||
customRender: render.renderImage,
|
||
},
|
||
```
|
||
|
||
**b) searchFormSchema 数组追加查询条件(仅常用查询字段需要):**
|
||
```typescript
|
||
{
|
||
label: '字段名称',
|
||
field: 'fieldName',
|
||
component: 'JInput', // 或 JDictSelectTag 等
|
||
colProps: { span: 6 },
|
||
},
|
||
```
|
||
|
||
**c) formSchema 数组末尾追加(在最后一个字段 `}` 后、`];` 前):**
|
||
```typescript
|
||
{
|
||
label: '字段名称',
|
||
field: 'fieldName',
|
||
component: 'Input', // 根据字段类型选择组件
|
||
componentProps: { placeholder: '请输入字段名称' },
|
||
},
|
||
```
|
||
|
||
**d) superQuerySchema 数组追加(如果存在):**
|
||
```typescript
|
||
{ title: '字段名称', value: 'fieldName', type: 'string' },
|
||
```
|
||
|
||
#### 3) *Form.vue — 仅 vue3Native 风格需要修改
|
||
|
||
在 `<a-form>` 中追加表单项:
|
||
```vue
|
||
<a-form-item label="字段名称" v-bind="validatorRules.fieldName" name="fieldName">
|
||
<a-input v-model:value="formData.fieldName" placeholder="请输入字段名称" />
|
||
</a-form-item>
|
||
```
|
||
|
||
在 `formData` reactive 对象中追加初始值:
|
||
```typescript
|
||
fieldName: '',
|
||
```
|
||
|
||
#### 4) Flyway SQL — 生成 ALTER TABLE
|
||
|
||
```sql
|
||
ALTER TABLE `{{tableName}}` ADD COLUMN `column_name` varchar(100) DEFAULT NULL COMMENT '字段注释';
|
||
```
|
||
|
||
多个字段可合并为一条 ALTER:
|
||
```sql
|
||
ALTER TABLE `{{tableName}}`
|
||
ADD COLUMN `field1` varchar(100) DEFAULT NULL COMMENT '注释1',
|
||
ADD COLUMN `field2` int DEFAULT NULL COMMENT '注释2';
|
||
```
|
||
|
||
### F3. 删字段 — 需要修改的位置
|
||
|
||
**从以下位置移除字段相关代码:**
|
||
|
||
1. **Entity.java** — 删除字段声明及其注解(@Excel、@Dict、@Schema、@JsonFormat 等)
|
||
2. ***.data.ts** — 删除 columns 中对应列、searchFormSchema 中对应项、formSchema 中对应项、superQuerySchema 中对应项
|
||
3. ***Form.vue**(vue3Native)— 删除 `<a-form-item>` 和 formData 中对应属性
|
||
4. **Flyway SQL** — 生成 `ALTER TABLE \`{{tableName}}\` DROP COLUMN \`column_name\`;`
|
||
|
||
**注意:** 删除 Entity 字段后检查是否有不再使用的 import(如删除了唯一的 BigDecimal 字段,则移除 BigDecimal import)。
|
||
|
||
### F4. 改字段 — 需要修改的位置
|
||
|
||
根据修改内容,可能需要改动:
|
||
|
||
- **改类型**:Entity 字段类型 + data.ts 组件类型 + Form.vue 控件 + ALTER TABLE MODIFY
|
||
- **改注释/标题**:Entity @Excel name + @Schema description + data.ts title/label
|
||
- **加/改字典**:Entity @Dict + data.ts 组件改为 JDictSelectTag + columns dataIndex 加 _dictText
|
||
- **改必填**:data.ts formSchema 中 required 属性
|
||
|
||
Flyway SQL 示例:
|
||
```sql
|
||
ALTER TABLE `{{tableName}}` MODIFY COLUMN `column_name` decimal(10,2) DEFAULT NULL COMMENT '新注释';
|
||
```
|
||
|
||
### F5. 增量修改检查清单
|
||
|
||
每次增量修改完成后,确认:
|
||
- [ ] Entity.java — 字段声明 + 注解 + import
|
||
- [ ] *.data.ts — columns + searchFormSchema(如需) + formSchema + superQuerySchema(如存在)
|
||
- [ ] *Form.vue — 表单控件 + formData 初始值(仅 vue3Native)
|
||
- [ ] Flyway SQL — ALTER TABLE 语句
|
||
- [ ] 无遗漏的 import 增删
|