mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-25 03:21:46 +00:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
effc3a3e69 | ||
|
|
08f789737f | ||
|
|
f57bf5d61d | ||
|
|
143dbf19a2 | ||
|
|
f6bd306d6d | ||
|
|
3beb00143a | ||
|
|
7a52a50964 | ||
|
|
630e13686c | ||
|
|
05661e2f3e | ||
|
|
3625ce851b | ||
|
|
8b312bed1d | ||
|
|
30bcb57f41 | ||
|
|
5bb211afcd | ||
|
|
d102c4b7c1 | ||
|
|
545453c93e | ||
|
|
c3d2a5952b | ||
|
|
d65b21baf6 | ||
|
|
0f31feae6f | ||
|
|
0494a27d6c | ||
|
|
e2b6ddb290 | ||
|
|
28eba92077 | ||
|
|
aa7a92664d | ||
|
|
68ecc5f9a8 | ||
|
|
8341044251 | ||
|
|
8b03400ecc | ||
|
|
8115c3a737 | ||
|
|
c01307202d | ||
|
|
5ecb1e6e4c | ||
|
|
9c299805c1 | ||
|
|
2a900c9876 | ||
|
|
0008b943a3 | ||
|
|
50c74b1f96 | ||
|
|
ae1eef6d4f | ||
|
|
d01cdc040f | ||
|
|
92b9af17b7 | ||
|
|
898e1b023a |
@@ -100,6 +100,12 @@ jobs:
|
||||
- name: Get dependencies
|
||||
run: go mod tidy
|
||||
|
||||
# Before the tests rather than beside checksilent at the end: a formatting
|
||||
# miss is a one-command fix, and finding out about it after five minutes of
|
||||
# tests and an end-to-end install is five minutes nobody gets back.
|
||||
- name: Formatting
|
||||
run: make fmt-check
|
||||
|
||||
# go build does not compile _test.go, so building alone never ran a single
|
||||
# test. This is the only workflow that fires on every push and pull request,
|
||||
# which makes it the one place a test gate belongs.
|
||||
|
||||
@@ -20,7 +20,7 @@ Router → Api → Service → Model
|
||||
|
||||
## 优先使用通用 Action
|
||||
|
||||
单表 CRUD **不要手写 Handler 与 Service**。`common/actions` 提供的五个
|
||||
单表 CRUD **不要手写 Api 与 Service**。`common/actions` 提供的五个
|
||||
Action 已覆盖参数绑定、数据权限过滤、操作人注入、分页与错误响应:
|
||||
|
||||
```go
|
||||
@@ -49,7 +49,7 @@ r := v1.Group("/demo-product").Use(authMiddleware.MiddlewareFunc()).Use(middlewa
|
||||
就地返回会串数据(`app/demo` 的测试锁定了这一点)
|
||||
- 详情/删除 DTO 内嵌 `dto.ObjectById` 即可继承 `Bind` 与 `GetId`,无需重写
|
||||
|
||||
仅当业务超出单表 CRUD(跨表事务、外部调用、复杂校验)时才自行编写 Handler
|
||||
仅当业务超出单表 CRUD(跨表事务、外部调用、复杂校验)时才自行编写 Api
|
||||
与 Service,写法见下。
|
||||
|
||||
## Api 层(仅在通用 Action 不适用时)
|
||||
@@ -167,7 +167,7 @@ sys_menu / sys_menu_api_rule / casbin_rule 四张表如何配齐,用的是幂
|
||||
|
||||
## Swagger
|
||||
|
||||
Handler 必须带完整注解,`go generate` 会据此生成文档:
|
||||
Api 必须带完整注解,`go generate` 会据此生成文档:
|
||||
|
||||
```go
|
||||
// @Summary 岗位列表
|
||||
|
||||
@@ -80,6 +80,23 @@ else
|
||||
go run ./tools/checksilent
|
||||
endif
|
||||
|
||||
# gofmt as a gate, not a rewrite. CI cannot commit, and a target that quietly
|
||||
# reformats hides what it touched, so this reports and fails instead. `gofmt -l`
|
||||
# prints the files it would rewrite and nothing at all when there are none, so
|
||||
# that list is both the failure message and the instructions for fixing it.
|
||||
#
|
||||
# The tree reached zero unformatted files once; without something holding it
|
||||
# there it drifts back, which is how the previous batch grew to 26 files -
|
||||
# mostly a missing newline at the end of the file, which no reviewer notices.
|
||||
.PHONY: fmt-check
|
||||
fmt-check:
|
||||
@unformatted=$$(gofmt -l .); \
|
||||
if [ -n "$$unformatted" ]; then \
|
||||
echo "gofmt would rewrite these files. Run 'gofmt -w .' and commit the result:"; \
|
||||
echo "$$unformatted"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
#.PHONY: docker
|
||||
#docker:
|
||||
# docker build . -t go-admin:latest
|
||||
|
||||
@@ -3,9 +3,9 @@ package apis
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/app/admin/service"
|
||||
@@ -145,4 +145,4 @@ func (e SysApi) DeleteSysApi(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
e.OK(req.GetId(), "删除成功")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package apis
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"go-admin/app/admin/models"
|
||||
|
||||
"go-admin/app/admin/service"
|
||||
@@ -216,5 +216,5 @@ func (e SysDictData) GetAll(c *gin.Context) {
|
||||
l = append(l, d)
|
||||
}
|
||||
|
||||
e.OK(l,"查询成功")
|
||||
e.OK(l, "查询成功")
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"go-admin/app/admin/models"
|
||||
|
||||
"go-admin/app/admin/service"
|
||||
@@ -31,7 +31,7 @@ type SysDictType struct {
|
||||
// @Security Bearer
|
||||
func (e SysDictType) GetPage(c *gin.Context) {
|
||||
s := service.SysDictType{}
|
||||
req :=dto.SysDictTypeGetPageReq{}
|
||||
req := dto.SysDictTypeGetPageReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, binding.Form).
|
||||
@@ -62,7 +62,7 @@ func (e SysDictType) GetPage(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
func (e SysDictType) Get(c *gin.Context) {
|
||||
s := service.SysDictType{}
|
||||
req :=dto.SysDictTypeGetReq{}
|
||||
req := dto.SysDictTypeGetReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, nil).
|
||||
@@ -82,7 +82,7 @@ func (e SysDictType) Get(c *gin.Context) {
|
||||
e.OK(object, "查询成功")
|
||||
}
|
||||
|
||||
//Insert 字典类型创建
|
||||
// Insert 字典类型创建
|
||||
// @Summary 添加字典类型
|
||||
// @Description 获取JSON
|
||||
// @Tags 字典类型
|
||||
@@ -94,7 +94,7 @@ func (e SysDictType) Get(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
func (e SysDictType) Insert(c *gin.Context) {
|
||||
s := service.SysDictType{}
|
||||
req :=dto.SysDictTypeInsertReq{}
|
||||
req := dto.SysDictTypeInsertReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, binding.JSON).
|
||||
@@ -109,7 +109,7 @@ func (e SysDictType) Insert(c *gin.Context) {
|
||||
err = s.Insert(&req)
|
||||
if err != nil {
|
||||
e.Logger.Error(err)
|
||||
e.Error(500, err,fmt.Sprintf(" 创建字典类型失败,详情:%s", err.Error()))
|
||||
e.Error(500, err, fmt.Sprintf(" 创建字典类型失败,详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
e.OK(req.GetId(), "创建成功")
|
||||
@@ -127,7 +127,7 @@ func (e SysDictType) Insert(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
func (e SysDictType) Update(c *gin.Context) {
|
||||
s := service.SysDictType{}
|
||||
req :=dto.SysDictTypeUpdateReq{}
|
||||
req := dto.SysDictTypeUpdateReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, binding.JSON, nil).
|
||||
@@ -157,7 +157,7 @@ func (e SysDictType) Update(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
func (e SysDictType) Delete(c *gin.Context) {
|
||||
s := service.SysDictType{}
|
||||
req :=dto.SysDictTypeDeleteReq{}
|
||||
req := dto.SysDictTypeDeleteReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, binding.JSON, nil).
|
||||
@@ -189,7 +189,7 @@ func (e SysDictType) Delete(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
func (e SysDictType) GetAll(c *gin.Context) {
|
||||
s := service.SysDictType{}
|
||||
req :=dto.SysDictTypeGetPageReq{}
|
||||
req := dto.SysDictTypeGetPageReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, binding.Form).
|
||||
@@ -207,4 +207,4 @@ func (e SysDictType) GetAll(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
e.OK(list, "查询成功")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ type SysLoginLog struct {
|
||||
// @Security Bearer
|
||||
func (e SysLoginLog) GetPage(c *gin.Context) {
|
||||
s := service.SysLoginLog{}
|
||||
req :=dto.SysLoginLogGetPageReq{}
|
||||
req := dto.SysLoginLogGetPageReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, binding.Form).
|
||||
@@ -60,7 +60,7 @@ func (e SysLoginLog) GetPage(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
func (e SysLoginLog) Get(c *gin.Context) {
|
||||
s := service.SysLoginLog{}
|
||||
req :=dto.SysLoginLogGetReq{}
|
||||
req := dto.SysLoginLogGetReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req).
|
||||
@@ -90,7 +90,7 @@ func (e SysLoginLog) Get(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
func (e SysLoginLog) Delete(c *gin.Context) {
|
||||
s := service.SysLoginLog{}
|
||||
req :=dto.SysLoginLogDeleteReq{}
|
||||
req := dto.SysLoginLogDeleteReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, binding.JSON, nil).
|
||||
@@ -107,4 +107,4 @@ func (e SysLoginLog) Delete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
e.OK(req.GetId(), "删除成功")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func (e SysOperaLog) GetPage(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
func (e SysOperaLog) Get(c *gin.Context) {
|
||||
s := new(service.SysOperaLog)
|
||||
req :=dto.SysOperaLogGetReq{}
|
||||
req := dto.SysOperaLogGetReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, nil).
|
||||
@@ -96,7 +96,7 @@ func (e SysOperaLog) Get(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
func (e SysOperaLog) Delete(c *gin.Context) {
|
||||
s := new(service.SysOperaLog)
|
||||
req :=dto.SysOperaLogDeleteReq{}
|
||||
req := dto.SysOperaLogDeleteReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, binding.JSON).
|
||||
@@ -111,7 +111,7 @@ func (e SysOperaLog) Delete(c *gin.Context) {
|
||||
err = s.Remove(&req)
|
||||
if err != nil {
|
||||
e.Logger.Error(err)
|
||||
e.Error(500,err, fmt.Sprintf("删除失败!错误详情:%s", err.Error()))
|
||||
e.Error(500, err, fmt.Sprintf("删除失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
e.OK(req.GetId(), "删除成功")
|
||||
|
||||
@@ -2,12 +2,12 @@ package apis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/app/admin/service"
|
||||
@@ -31,7 +31,7 @@ type SysPost struct {
|
||||
// @Security Bearer
|
||||
func (e SysPost) GetPage(c *gin.Context) {
|
||||
s := service.SysPost{}
|
||||
req :=dto.SysPostPageReq{}
|
||||
req := dto.SysPostPageReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, binding.Form).
|
||||
@@ -65,7 +65,7 @@ func (e SysPost) GetPage(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
func (e SysPost) Get(c *gin.Context) {
|
||||
s := service.SysPost{}
|
||||
req :=dto.SysPostGetReq{}
|
||||
req := dto.SysPostGetReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, nil).
|
||||
@@ -99,7 +99,7 @@ func (e SysPost) Get(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
func (e SysPost) Insert(c *gin.Context) {
|
||||
s := service.SysPost{}
|
||||
req :=dto.SysPostInsertReq{}
|
||||
req := dto.SysPostInsertReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, binding.JSON).
|
||||
@@ -131,7 +131,7 @@ func (e SysPost) Insert(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
func (e SysPost) Update(c *gin.Context) {
|
||||
s := service.SysPost{}
|
||||
req :=dto.SysPostUpdateReq{}
|
||||
req := dto.SysPostUpdateReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, binding.JSON, nil).
|
||||
@@ -163,7 +163,7 @@ func (e SysPost) Update(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
func (e SysPost) Delete(c *gin.Context) {
|
||||
s := service.SysPost{}
|
||||
req :=dto.SysPostDeleteReq{}
|
||||
req := dto.SysPostDeleteReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req, binding.JSON).
|
||||
@@ -181,4 +181,4 @@ func (e SysPost) Delete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
e.OK(req.GetId(), "删除成功")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,4 @@ func registerSysDeptRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddle
|
||||
r1.GET("/deptTree", api.Get2Tree)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,4 @@ func registerSysLoginLogRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMi
|
||||
r.GET("/:id", api.Get)
|
||||
r.DELETE("", api.Delete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,4 +30,4 @@ func registerSysMenuRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddle
|
||||
//r1.GET("/menuids", api.GetMenuIDS)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,4 @@ func registerSysOperaLogRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMi
|
||||
r.GET("/:id", api.Get)
|
||||
r.DELETE("", api.Delete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,4 @@ func registerSyPostRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddlew
|
||||
r.PUT("/:id", api.Update)
|
||||
r.DELETE("", api.Delete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,4 @@ func registerSysUserRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddle
|
||||
{
|
||||
v1auth.GET("/getinfo", api.GetInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,15 @@ import (
|
||||
|
||||
// SysDeptGetPageReq 列表或者搜索使用结构体
|
||||
type SysDeptGetPageReq struct {
|
||||
DeptId int `form:"deptId" search:"type:exact;column:dept_id;table:sys_dept" comment:"id"` //id
|
||||
ParentId int `form:"parentId" search:"type:exact;column:parent_id;table:sys_dept" comment:"上级部门"` //上级部门
|
||||
DeptPath string `form:"deptPath" search:"type:exact;column:dept_path;table:sys_dept" comment:""` //路径
|
||||
DeptName string `form:"deptName" search:"type:exact;column:dept_name;table:sys_dept" comment:"部门名称"` //部门名称
|
||||
Sort int `form:"sort" search:"type:exact;column:sort;table:sys_dept" comment:"排序"` //排序
|
||||
Leader string `form:"leader" search:"type:exact;column:leader;table:sys_dept" comment:"负责人"` //负责人
|
||||
Phone string `form:"phone" search:"type:exact;column:phone;table:sys_dept" comment:"手机"` //手机
|
||||
Email string `form:"email" search:"type:exact;column:email;table:sys_dept" comment:"邮箱"` //邮箱
|
||||
Status string `form:"status" search:"type:exact;column:status;table:sys_dept" comment:"状态"` //状态
|
||||
DeptId int `form:"deptId" search:"type:exact;column:dept_id;table:sys_dept" comment:"id"` //id
|
||||
ParentId int `form:"parentId" search:"type:exact;column:parent_id;table:sys_dept" comment:"上级部门"` //上级部门
|
||||
DeptPath string `form:"deptPath" search:"type:exact;column:dept_path;table:sys_dept" comment:""` //路径
|
||||
DeptName string `form:"deptName" search:"type:exact;column:dept_name;table:sys_dept" comment:"部门名称"` //部门名称
|
||||
Sort int `form:"sort" search:"type:exact;column:sort;table:sys_dept" comment:"排序"` //排序
|
||||
Leader string `form:"leader" search:"type:exact;column:leader;table:sys_dept" comment:"负责人"` //负责人
|
||||
Phone string `form:"phone" search:"type:exact;column:phone;table:sys_dept" comment:"手机"` //手机
|
||||
Email string `form:"email" search:"type:exact;column:email;table:sys_dept" comment:"邮箱"` //邮箱
|
||||
Status string `form:"status" search:"type:exact;column:status;table:sys_dept" comment:"状态"` //状态
|
||||
}
|
||||
|
||||
func (m *SysDeptGetPageReq) GetNeedSearch() interface{} {
|
||||
|
||||
@@ -54,4 +54,4 @@ type SysLoginLogDeleteReq struct {
|
||||
|
||||
func (s *SysLoginLogDeleteReq) GetId() interface{} {
|
||||
return s.Ids
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,4 +107,4 @@ type SysRoleMenu struct {
|
||||
// return nil, err
|
||||
// }
|
||||
// return r, nil
|
||||
//}
|
||||
//}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go-admin/app/other/models/tools"
|
||||
)
|
||||
|
||||
// columnLengthPattern pulls the first parenthesized integer out of a MySQL
|
||||
// COLUMN_TYPE string - the "(255)" in "varchar(255)", the "(10" in
|
||||
// "decimal(10,2)". Works regardless of trailing modifiers such as
|
||||
// "unsigned" or a charset clause, since it only looks for the first digits
|
||||
// after the first '('.
|
||||
var columnLengthPattern = regexp.MustCompile(`\((\d+)`)
|
||||
|
||||
// InferColumnWidth backs R2's fallback path: when a column's colWidth is
|
||||
// left at its 0 sentinel (unconfigured), this reads sys_columns.column_type
|
||||
// - MySQL's information_schema.COLUMNS.COLUMN_TYPE, which carries length,
|
||||
// e.g. "varchar(255)", "int(11)", "decimal(10,2)", "tinyint(1)" - and
|
||||
// returns a px width sized to fit inside go-admin-ui's ~580px text-column
|
||||
// budget for a 1280px viewport (its AGENTS.md "列宽" section).
|
||||
//
|
||||
// The judgment has to be columnType, not goType: sys_tables.go:323-338
|
||||
// gives every non-primary-key int/tinyint/bigint/decimal column goType
|
||||
// "string" (a bare substring match on "int" that also catches "tinyint"/
|
||||
// "bigint", intentional at import time but useless for telling a boolean
|
||||
// flag from a bigint), so goType alone cannot distinguish a switch column
|
||||
// from a price column from a name column. This is the same judgment call
|
||||
// API契约.md §1.1 made, reversing the PRD's original "GoType" reading of R2.
|
||||
// GoType is not consulted anywhere in this function, including for
|
||||
// datetime/timestamp columns - those are matched on columnType too.
|
||||
//
|
||||
// Exported and pure (string in, int out) so QA can pin an exact input/output
|
||||
// table against it directly (测试用例.md §2.5's own recommendation), rather
|
||||
// than only being able to assert "the rendered page happens not to overflow".
|
||||
func InferColumnWidth(columnType string) int {
|
||||
ct := strings.ToLower(strings.TrimSpace(columnType))
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(ct, "tinyint(1)"):
|
||||
// MySQL's own shape for a boolean/status flag - a tag or a switch,
|
||||
// not text, so it wants less room than a general numeric column.
|
||||
return 70
|
||||
|
||||
case strings.Contains(ct, "datetime"), strings.Contains(ct, "timestamp"),
|
||||
strings.Contains(ct, "date"), strings.Contains(ct, "time"):
|
||||
return 110
|
||||
|
||||
case strings.HasPrefix(ct, "tinyint"), strings.HasPrefix(ct, "smallint"),
|
||||
strings.HasPrefix(ct, "mediumint"), strings.HasPrefix(ct, "int"),
|
||||
strings.HasPrefix(ct, "bigint"), strings.HasPrefix(ct, "decimal"),
|
||||
strings.HasPrefix(ct, "float"), strings.HasPrefix(ct, "double"):
|
||||
// API契约.md §1.1: "decimal/bigint/int 类给数字型窄宽度" groups these
|
||||
// together rather than sizing each individually - none of them need
|
||||
// more than a handful of digits' worth of width.
|
||||
return 90
|
||||
|
||||
case strings.HasPrefix(ct, "varchar"), strings.HasPrefix(ct, "char"):
|
||||
return varcharWidth(columnLength(ct))
|
||||
|
||||
case strings.Contains(ct, "text"), strings.Contains(ct, "blob"):
|
||||
// longtext/mediumtext/text/blob: no declared length to size against,
|
||||
// and content here is free-form, so this errs wide rather than
|
||||
// guessing a number the actual content will not respect.
|
||||
return 260
|
||||
|
||||
default:
|
||||
// Unrecognized column_type (an enum, a json column, a driver this
|
||||
// codebase does not special-case, ...). Matches the flat fallback
|
||||
// vue.go.template already used for every non-datetime column before
|
||||
// this function existed, so a type this does not recognize is no
|
||||
// worse off than the old blanket default.
|
||||
return 120
|
||||
}
|
||||
}
|
||||
|
||||
// varcharWidth tiers a char/varchar column by its declared length. The
|
||||
// tiers are deliberately coarse - R2 only asks for "common tables land in
|
||||
// the 580px budget", not pixel-perfect sizing per character.
|
||||
func varcharWidth(n int) int {
|
||||
switch {
|
||||
case n <= 0:
|
||||
// Length did not parse (unexpected shape) - mid tier, not the
|
||||
// narrowest, since an un-lengthed varchar is unlikely to be a
|
||||
// short code column.
|
||||
return 150
|
||||
case n <= 10:
|
||||
return 90
|
||||
case n <= 20:
|
||||
return 110
|
||||
case n <= 50:
|
||||
return 150
|
||||
case n <= 100:
|
||||
return 200
|
||||
default:
|
||||
return 240
|
||||
}
|
||||
}
|
||||
|
||||
// columnLength extracts the first parenthesized integer, or 0 if the type
|
||||
// string does not have one (already-lowercased input expected).
|
||||
func columnLength(columnType string) int {
|
||||
m := columnLengthPattern.FindStringSubmatch(columnType)
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
n, err := strconv.Atoi(m[1])
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// applyInferredColumnWidths fills in InferColumnWidth's result for every
|
||||
// column still at the 0 "unconfigured" sentinel, in place, before the
|
||||
// template that reads .ColWidth runs. A column the user (or F6's config
|
||||
// page) already gave an explicit width is left untouched.
|
||||
func applyInferredColumnWidths(columns []tools.SysColumns) {
|
||||
for i := range columns {
|
||||
if columns[i].ColWidth == 0 {
|
||||
columns[i].ColWidth = InferColumnWidth(columns[i].ColumnType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go-admin/app/other/models/tools"
|
||||
)
|
||||
|
||||
// Input/output pins for InferColumnWidth, per 测试用例.md §2.5's own
|
||||
// recommendation ("QA 才能在阶段 4 补一张精确的输入→输出对照表断言, 而不是只测
|
||||
// 结果凑巧没溢出这种弱结论") - this is that table, kept next to the function
|
||||
// it pins rather than only living in a later QA-owned suite.
|
||||
func TestInferColumnWidth(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
columnType string
|
||||
want int
|
||||
}{
|
||||
{"boolean/status flag", "tinyint(1)", 70},
|
||||
{"boolean flag, case-insensitive", "TINYINT(1)", 70},
|
||||
{"datetime", "datetime", 110},
|
||||
{"timestamp", "timestamp", 110},
|
||||
{"date only", "date", 110},
|
||||
{"time only", "time", 110},
|
||||
|
||||
{"plain tinyint (not the (1) boolean shape)", "tinyint(4)", 90},
|
||||
{"smallint", "smallint(6)", 90},
|
||||
{"mediumint", "mediumint(9)", 90},
|
||||
{"int", "int(11)", 90},
|
||||
{"bigint", "bigint(20)", 90},
|
||||
{"decimal", "decimal(10,2)", 90},
|
||||
{"float", "float", 90},
|
||||
{"double", "double", 90},
|
||||
|
||||
{"varchar short code", "varchar(8)", 90},
|
||||
{"varchar at the 10 boundary", "varchar(10)", 90},
|
||||
{"varchar just past the 10 boundary", "varchar(11)", 110},
|
||||
{"varchar at the 20 boundary", "varchar(20)", 110},
|
||||
{"varchar mid length", "varchar(32)", 150},
|
||||
{"varchar at the 50 boundary", "varchar(50)", 150},
|
||||
{"varchar just past the 50 boundary", "varchar(51)", 200},
|
||||
{"varchar(255), the common default", "varchar(255)", 240},
|
||||
{"char, fixed-width", "char(2)", 90},
|
||||
{"varchar with no parsed length", "varchar", 150},
|
||||
|
||||
{"text, no length to size against", "text", 260},
|
||||
{"longtext", "longtext", 260},
|
||||
{"mediumtext", "mediumtext", 260},
|
||||
{"blob", "blob", 260},
|
||||
|
||||
{"unrecognized type falls back to the old flat default", "json", 120},
|
||||
{"empty column_type falls back to the old flat default", "", 120},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := InferColumnWidth(tc.columnType); got != tc.want {
|
||||
t.Errorf("InferColumnWidth(%q) = %d, want %d", tc.columnType, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyInferredColumnWidths(t *testing.T) {
|
||||
columns := []tools.SysColumns{
|
||||
{JsonField: "name", ColumnType: "varchar(64)", ColWidth: 0},
|
||||
{JsonField: "price", ColumnType: "decimal(10,2)", ColWidth: 300}, // already configured
|
||||
}
|
||||
|
||||
applyInferredColumnWidths(columns)
|
||||
|
||||
if columns[0].ColWidth == 0 {
|
||||
t.Error("unconfigured column: want an inferred non-zero width, still 0")
|
||||
}
|
||||
if want := InferColumnWidth("varchar(64)"); columns[0].ColWidth != want {
|
||||
t.Errorf("unconfigured column: want %d (InferColumnWidth's own answer), got %d", want, columns[0].ColWidth)
|
||||
}
|
||||
if columns[1].ColWidth != 300 {
|
||||
t.Errorf("already-configured column: want the user's 300 left untouched, got %d", columns[1].ColWidth)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,12 @@ import (
|
||||
"go-admin/app/other/models/tools"
|
||||
)
|
||||
|
||||
// emptyTableNameMsg is what the generator's endpoints answer with when the
|
||||
// request named no table. Declared once because the tests assert on it: spelled
|
||||
// out again at each site, a reworded message would leave them asserting on a
|
||||
// string the server no longer sends, and still passing.
|
||||
const emptyTableNameMsg = "table name cannot be empty!"
|
||||
|
||||
// GetDBColumnList 分页列表数据
|
||||
// @Summary 分页列表数据 / page list data
|
||||
// @Description 数据库表列分页列表 / database table column page list
|
||||
@@ -41,7 +47,7 @@ func (e Gen) GetDBColumnList(c *gin.Context) {
|
||||
}
|
||||
|
||||
data.TableName = c.Request.FormValue("tableName")
|
||||
pkg.Assert(data.TableName != "", "table name cannot be empty!", 500)
|
||||
pkg.Assert(data.TableName != "", emptyTableNameMsg, 500)
|
||||
result, count, err := data.GetPage(db, pageSize, pageIndex)
|
||||
if err != nil {
|
||||
log.Errorf("GetPage error, %s", err.Error())
|
||||
|
||||
@@ -16,17 +16,21 @@ import (
|
||||
"go-admin/common/middleware"
|
||||
)
|
||||
|
||||
const emptyTableNameMsg = "table name cannot be empty!"
|
||||
|
||||
// bodyOf covers both the success and the CustomError shape: both carry msg.
|
||||
type bodyOf struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
// newColumnListEngine wires the handler the way the router does, including the
|
||||
// newEngine wires one generator handler the way the router does, including the
|
||||
// middleware that turns pkg.Assert's panic into a response.
|
||||
func newColumnListEngine(t *testing.T) *gin.Engine {
|
||||
//
|
||||
// The generator's queries target MySQL's information_schema and cannot run on
|
||||
// the sqlite connection behind them; the driver setting only has to select that
|
||||
// branch, since no statement here is expected to succeed. That makes this
|
||||
// serviceable for any handler in this package whose behaviour is decided before
|
||||
// the query goes out -- which is what these tests are about.
|
||||
func newEngine(t *testing.T, method, path string, h gin.HandlerFunc) *gin.Engine {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
@@ -35,26 +39,33 @@ func newColumnListEngine(t *testing.T) *gin.Engine {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
|
||||
// The query targets MySQL's information_schema; the driver setting only has
|
||||
// to select that branch, the statement itself is never expected to succeed.
|
||||
previous := config.DatabaseConfig.Driver
|
||||
config.DatabaseConfig.Driver = "mysql"
|
||||
t.Cleanup(func() { config.DatabaseConfig.Driver = previous })
|
||||
|
||||
r := gin.New()
|
||||
r.Use(middleware.CustomError)
|
||||
r.GET("/db/columns/page", func(c *gin.Context) {
|
||||
r.Handle(method, path, func(c *gin.Context) {
|
||||
c.Set("db", db)
|
||||
c.Set(pkg.LoggerKey, logger.NewHelper(logger.DefaultLogger))
|
||||
Gen{}.GetDBColumnList(c)
|
||||
h(c)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func columnListMsg(t *testing.T, r *gin.Engine, query string) bodyOf {
|
||||
func newColumnListEngine(t *testing.T) *gin.Engine {
|
||||
t.Helper()
|
||||
return newEngine(t, http.MethodGet, "/db/columns/page", Gen{}.GetDBColumnList)
|
||||
}
|
||||
|
||||
// serveJSON runs one request through the engine and decodes the envelope every
|
||||
// handler here answers with. A body that will not decode fails the test rather
|
||||
// than being reported as a mismatched message, which reads as the handler
|
||||
// having answered something unexpected instead of not having answered at all.
|
||||
func serveJSON(t *testing.T, r *gin.Engine, req *http.Request) bodyOf {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/db/columns/page"+query, nil))
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
var body bodyOf
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||||
@@ -63,6 +74,11 @@ func columnListMsg(t *testing.T, r *gin.Engine, query string) bodyOf {
|
||||
return body
|
||||
}
|
||||
|
||||
func columnListMsg(t *testing.T, r *gin.Engine, query string) bodyOf {
|
||||
t.Helper()
|
||||
return serveJSON(t, r, httptest.NewRequest(http.MethodGet, "/db/columns/page"+query, nil))
|
||||
}
|
||||
|
||||
func TestGetDBColumnList_AcceptsATableName(t *testing.T) {
|
||||
body := columnListMsg(t, newColumnListEngine(t), "?tableName=sys_user")
|
||||
if body.Msg == emptyTableNameMsg {
|
||||
|
||||
+104
-10
@@ -22,6 +22,29 @@ type Gen struct {
|
||||
api.Api
|
||||
}
|
||||
|
||||
// genLangFuncs backs the lang-zh/lang-en templates (PRD 010 F3/F9). The
|
||||
// generated files are TypeScript, and go-admin-ui's eslint config requires
|
||||
// single-quoted strings with no trailing comma (@stylistic/quotes,
|
||||
// @stylistic/comma-dangle: never) - text/template's builtin `printf "%q"`
|
||||
// only produces Go/JSON-style double-quoted output, so this supplies a
|
||||
// single-quote equivalent instead of leaning on the builtin.
|
||||
var genLangFuncs = template.FuncMap{
|
||||
"singleQuote": func(s string) string {
|
||||
r := strings.NewReplacer(`\`, `\\`, `'`, `\'`, "\n", `\n`, "\r", `\r`)
|
||||
return "'" + r.Replace(s) + "'"
|
||||
},
|
||||
}
|
||||
|
||||
// parseGenTemplate is template.ParseFiles plus genLangFuncs, for the two
|
||||
// language-pack templates. template.New's name must match the file's base
|
||||
// name - ParseFiles reuses the template already registered under that name
|
||||
// instead of creating an unnamed second one, which is what makes Execute
|
||||
// find the parsed content afterwards.
|
||||
func parseGenTemplate(path string) (*template.Template, error) {
|
||||
base := path[strings.LastIndex(path, "/")+1:]
|
||||
return template.New(base).Funcs(genLangFuncs).ParseFiles(path)
|
||||
}
|
||||
|
||||
func (e Gen) Preview(c *gin.Context) {
|
||||
e.Context = c
|
||||
log := e.GetLogger()
|
||||
@@ -45,10 +68,10 @@ func (e Gen) Preview(c *gin.Context) {
|
||||
e.Error(500, err, fmt.Sprintf("api模版读取失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
t3, err := template.ParseFiles("template/v4/js.go.template")
|
||||
t3, err := template.ParseFiles("template/v4/ts.go.template")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
e.Error(500, err, fmt.Sprintf("js模版读取失败!错误详情:%s", err.Error()))
|
||||
e.Error(500, err, fmt.Sprintf("ts模版读取失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
t4, err := template.ParseFiles("template/v4/vue.go.template")
|
||||
@@ -75,6 +98,22 @@ func (e Gen) Preview(c *gin.Context) {
|
||||
e.Error(500, err, fmt.Sprintf("service模版读取失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
// t8/t9 back F3/F9 (PRD 010): one language pack per locale, nested under
|
||||
// gen/{PackageName}/{BusinessName}.ts by NOActionsGen below so go-admin-ui's
|
||||
// gen-namespace.ts glob (`./*/*.ts` under each locale's gen/) picks them up.
|
||||
// See docs-prd/010-代码生成器前端模板迁移Vue3/API契约.md §2.3.
|
||||
t8, err := parseGenTemplate("template/v4/lang-zh.go.template")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
e.Error(500, err, fmt.Sprintf("zh语言包模版读取失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
t9, err := parseGenTemplate("template/v4/lang-en.go.template")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
e.Error(500, err, fmt.Sprintf("en语言包模版读取失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
db, err := pkg.GetOrm(c)
|
||||
if err != nil {
|
||||
@@ -83,7 +122,18 @@ func (e Gen) Preview(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
tab, _ := table.Get(db,false)
|
||||
tab, _ := table.Get(db, false)
|
||||
// MLTBName (table_name with underscores turned to dashes) is a gorm:"-"
|
||||
// field - table.Get never fills it in, so every template that reads it
|
||||
// (the .vue/.ts import paths, e.g. "@/api/{PackageName}/{MLTBName}")
|
||||
// silently rendered it empty here. NOActionsGen has set this since it
|
||||
// existed (see below); Preview never did, which is why the two paths
|
||||
// are not interchangeable stand-ins for each other and should not be
|
||||
// assumed to be.
|
||||
tab.MLTBName = strings.Replace(tab.TBName, "_", "-", -1)
|
||||
// R2: infer a width for any column the config page left at colWidth's 0
|
||||
// sentinel, before vue.go.template reads .ColWidth - see column_width.go.
|
||||
applyInferredColumnWidths(tab.Columns)
|
||||
var b1 bytes.Buffer
|
||||
err = t1.Execute(&b1, tab)
|
||||
var b2 bytes.Buffer
|
||||
@@ -98,15 +148,21 @@ func (e Gen) Preview(c *gin.Context) {
|
||||
err = t6.Execute(&b6, tab)
|
||||
var b7 bytes.Buffer
|
||||
err = t7.Execute(&b7, tab)
|
||||
var b8 bytes.Buffer
|
||||
err = t8.Execute(&b8, tab)
|
||||
var b9 bytes.Buffer
|
||||
err = t9.Execute(&b9, tab)
|
||||
|
||||
mp := make(map[string]interface{})
|
||||
mp["template/model.go.template"] = b1.String()
|
||||
mp["template/api.go.template"] = b2.String()
|
||||
mp["template/js.go.template"] = b3.String()
|
||||
mp["template/api.ts.template"] = b3.String()
|
||||
mp["template/vue.go.template"] = b4.String()
|
||||
mp["template/router.go.template"] = b5.String()
|
||||
mp["template/dto.go.template"] = b6.String()
|
||||
mp["template/service.go.template"] = b7.String()
|
||||
mp["template/lang-zh.go.template"] = b8.String()
|
||||
mp["template/lang-en.go.template"] = b9.String()
|
||||
e.OK(mp, "")
|
||||
}
|
||||
|
||||
@@ -129,7 +185,7 @@ func (e Gen) GenCode(c *gin.Context) {
|
||||
}
|
||||
|
||||
table.TableId = id
|
||||
tab, _ := table.Get(db,false)
|
||||
tab, _ := table.Get(db, false)
|
||||
|
||||
e.NOActionsGen(c, tab)
|
||||
|
||||
@@ -155,7 +211,7 @@ func (e Gen) GenApiToFile(c *gin.Context) {
|
||||
}
|
||||
|
||||
table.TableId = id
|
||||
tab, _ := table.Get(db,false)
|
||||
tab, _ := table.Get(db, false)
|
||||
e.genApiToFile(c, tab)
|
||||
|
||||
e.OK("", "Code generated successfully!")
|
||||
@@ -165,6 +221,8 @@ func (e Gen) NOActionsGen(c *gin.Context, tab tools.SysTables) {
|
||||
e.Context = c
|
||||
log := e.GetLogger()
|
||||
tab.MLTBName = strings.Replace(tab.TBName, "_", "-", -1)
|
||||
// R2: see the matching call and comment in Preview above.
|
||||
applyInferredColumnWidths(tab.Columns)
|
||||
|
||||
basePath := "template/v4/"
|
||||
routerFile := basePath + "no_actions/router_check_role.go.template"
|
||||
@@ -191,10 +249,10 @@ func (e Gen) NOActionsGen(c *gin.Context, tab tools.SysTables) {
|
||||
e.Error(500, err, fmt.Sprintf("路由模版失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
t4, err := template.ParseFiles(basePath + "js.go.template")
|
||||
t4, err := template.ParseFiles(basePath + "ts.go.template")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
e.Error(500, err, fmt.Sprintf("js模版解析失败!错误详情:%s", err.Error()))
|
||||
e.Error(500, err, fmt.Sprintf("ts模版解析失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
t5, err := template.ParseFiles(basePath + "vue.go.template")
|
||||
@@ -215,6 +273,19 @@ func (e Gen) NOActionsGen(c *gin.Context, tab tools.SysTables) {
|
||||
e.Error(500, err, fmt.Sprintf("service模版失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
// t8/t9 back F3/F9 (PRD 010): see the matching comment in Preview above.
|
||||
t8, err := parseGenTemplate(basePath + "lang-zh.go.template")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
e.Error(500, err, fmt.Sprintf("zh语言包模版解析失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
t9, err := parseGenTemplate(basePath + "lang-en.go.template")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
e.Error(500, err, fmt.Sprintf("en语言包模版解析失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
_ = pkg.PathCreate("./app/" + tab.PackageName + "/apis/")
|
||||
_ = pkg.PathCreate("./app/" + tab.PackageName + "/models/")
|
||||
@@ -227,6 +298,23 @@ func (e Gen) NOActionsGen(c *gin.Context, tab tools.SysTables) {
|
||||
e.Error(500, err, fmt.Sprintf("views目录创建失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
// gen/{PackageName}/ nests under each locale so go-admin-ui's
|
||||
// gen-namespace.ts (`./*/*.ts` glob, one level under gen/) picks the file
|
||||
// up - a flat gen/{BusinessName}.ts would let two tables in different
|
||||
// packages silently overwrite each other's translations, since
|
||||
// BusinessName only has a pattern check, no uniqueness check.
|
||||
err = pkg.PathCreate(config.GenConfig.FrontPath + "/lang/zh-CN/gen/" + tab.PackageName + "/")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
e.Error(500, err, fmt.Sprintf("zh语言包目录创建失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
err = pkg.PathCreate(config.GenConfig.FrontPath + "/lang/en-US/gen/" + tab.PackageName + "/")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
e.Error(500, err, fmt.Sprintf("en语言包目录创建失败!错误详情:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var b1 bytes.Buffer
|
||||
err = t1.Execute(&b1, tab)
|
||||
@@ -242,13 +330,19 @@ func (e Gen) NOActionsGen(c *gin.Context, tab tools.SysTables) {
|
||||
err = t6.Execute(&b6, tab)
|
||||
var b7 bytes.Buffer
|
||||
err = t7.Execute(&b7, tab)
|
||||
var b8 bytes.Buffer
|
||||
err = t8.Execute(&b8, tab)
|
||||
var b9 bytes.Buffer
|
||||
err = t9.Execute(&b9, tab)
|
||||
pkg.FileCreate(b1, "./app/"+tab.PackageName+"/models/"+tab.TBName+".go")
|
||||
pkg.FileCreate(b2, "./app/"+tab.PackageName+"/apis/"+tab.TBName+".go")
|
||||
pkg.FileCreate(b3, "./app/"+tab.PackageName+"/router/"+tab.TBName+".go")
|
||||
pkg.FileCreate(b4, config.GenConfig.FrontPath+"/api/"+tab.PackageName+"/"+tab.MLTBName+".js")
|
||||
pkg.FileCreate(b4, config.GenConfig.FrontPath+"/api/"+tab.PackageName+"/"+tab.MLTBName+".ts")
|
||||
pkg.FileCreate(b5, config.GenConfig.FrontPath+"/views/"+tab.PackageName+"/"+tab.MLTBName+"/index.vue")
|
||||
pkg.FileCreate(b6, "./app/"+tab.PackageName+"/service/dto/"+tab.TBName+".go")
|
||||
pkg.FileCreate(b7, "./app/"+tab.PackageName+"/service/"+tab.TBName+".go")
|
||||
pkg.FileCreate(b8, config.GenConfig.FrontPath+"/lang/zh-CN/gen/"+tab.PackageName+"/"+tab.BusinessName+".ts")
|
||||
pkg.FileCreate(b9, config.GenConfig.FrontPath+"/lang/en-US/gen/"+tab.PackageName+"/"+tab.BusinessName+".ts")
|
||||
|
||||
}
|
||||
|
||||
@@ -302,7 +396,7 @@ func (e Gen) GenMenuAndApi(c *gin.Context) {
|
||||
}
|
||||
|
||||
table.TableId = id
|
||||
tab, _ := table.Get(e.Orm,true)
|
||||
tab, _ := table.Get(e.Orm, true)
|
||||
tab.MLTBName = strings.Replace(tab.TBName, "_", "-", -1)
|
||||
|
||||
Mmenu := dto.SysMenuInsertReq{}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/app/other/models/tools"
|
||||
@@ -79,7 +80,7 @@ func (e SysTable) Get(c *gin.Context) {
|
||||
|
||||
var data tools.SysTables
|
||||
data.TableId, _ = pkg.StringToInt(c.Param("tableId"))
|
||||
result, err := data.Get(db,true)
|
||||
result, err := data.Get(db, true)
|
||||
if err != nil {
|
||||
log.Errorf("Get error, %s", err.Error())
|
||||
e.Error(500, err, "")
|
||||
@@ -106,7 +107,7 @@ func (e SysTable) GetSysTablesInfo(c *gin.Context) {
|
||||
if c.Request.FormValue("tableName") != "" {
|
||||
data.TBName = c.Request.FormValue("tableName")
|
||||
}
|
||||
result, err := data.Get(db,true)
|
||||
result, err := data.Get(db, true)
|
||||
if err != nil {
|
||||
log.Errorf("Get error, %s", err.Error())
|
||||
e.Error(500, err, "抱歉未找到相关信息")
|
||||
@@ -148,7 +149,8 @@ func (e SysTable) GetSysTablesTree(c *gin.Context) {
|
||||
// @Tags 工具 / 生成工具
|
||||
// @Accept application/json
|
||||
// @Product application/json
|
||||
// @Param tables query string false "tableName / 数据表名称"
|
||||
// @Param tables query string false "tableName / 数据表名称,逗号分隔"
|
||||
// @Param data body object false "tables / 同上,query 未带时从 JSON body 读"
|
||||
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
|
||||
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
|
||||
// @Router /api/v1/sys/tables/info [post]
|
||||
@@ -163,7 +165,13 @@ func (e SysTable) Insert(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
tablesList := strings.Split(c.Request.FormValue("tables"), ",")
|
||||
tablesList, err := tablesToImport(c)
|
||||
if err != nil {
|
||||
log.Errorf("read the table list, %s", err.Error())
|
||||
e.Error(500, err, "")
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < len(tablesList); i++ {
|
||||
|
||||
data, err := genTableInit(db, tablesList, i, c)
|
||||
@@ -184,6 +192,45 @@ func (e SysTable) Insert(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
// tablesToImport reads the comma-separated table list carried by an import
|
||||
// request, from the query string or from a JSON body.
|
||||
//
|
||||
// The list has only ever travelled in the query string, which is the single
|
||||
// place FormValue looks once the request declares itself as JSON. A front end
|
||||
// that puts it in the body instead therefore left this empty, and the import
|
||||
// went on to ask information_schema for a table named "" -- go-admin-ui v3.2.0
|
||||
// shipped exactly that, and every import failed with the message below.
|
||||
// Reading the body when the query has nothing keeps either front end working.
|
||||
func tablesToImport(c *gin.Context) ([]string, error) {
|
||||
raw := c.Request.FormValue("tables")
|
||||
if raw == "" {
|
||||
var body struct {
|
||||
Tables string `json:"tables"`
|
||||
}
|
||||
// A body that is absent, or shaped some other way, is not itself worth
|
||||
// reporting: the list is missing either way, and the message below says
|
||||
// so in the terms the caller asked in.
|
||||
if err := c.ShouldBindJSON(&body); err == nil {
|
||||
raw = body.Tables
|
||||
}
|
||||
}
|
||||
|
||||
parts := strings.Split(raw, ",")
|
||||
names := make([]string, 0, len(parts))
|
||||
for _, name := range parts {
|
||||
// Splitting "" yields one empty name rather than nothing at all, so
|
||||
// without this an empty list reads as a request to import one table
|
||||
// whose name happens to be blank.
|
||||
if name = strings.TrimSpace(name); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return nil, errors.New(emptyTableNameMsg)
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func genTableInit(tx *gorm.DB, tablesList []string, i int, c *gin.Context) (tools.SysTables, error) {
|
||||
var data tools.SysTables
|
||||
var dbTable tools.DBTables
|
||||
@@ -321,6 +368,21 @@ func (e SysTable) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// PRD 010 F10: this bind-and-save path has no field-level validation of
|
||||
// its own (API契约.md §1.2/§2.1, D6) - see sys_tables_validate.go for
|
||||
// what each check guards and why colWidth is sanitized in place rather
|
||||
// than rejected.
|
||||
if err = validateAndSanitizeColumns(data.Columns); err != nil {
|
||||
log.Errorf("validate columns error, %s", err.Error())
|
||||
e.Error(500, err, err.Error())
|
||||
return
|
||||
}
|
||||
if err = validateBusinessNameUnique(db, data.PackageName, data.BusinessName, data.TableId); err != nil {
|
||||
log.Errorf("validate businessName error, %s", err.Error())
|
||||
e.Error(500, err, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data.UpdateBy = 0
|
||||
result, err := data.Update(db)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// newImportRequest builds the request an import arrives in. Where the table
|
||||
// list sits -- query or body -- is exactly what these tests are about, and it
|
||||
// is net/http's form parsing that decides what a handler can reach, so these go
|
||||
// through a real *http.Request rather than a hand-built one.
|
||||
func newImportRequest(target, contentType, body string) *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, target, strings.NewReader(body))
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func TestTablesToImport(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
target string
|
||||
contentType string
|
||||
body string
|
||||
want []string
|
||||
wantErr bool
|
||||
}{{
|
||||
name: "from the query, as every front end before v3.2.0 sent it",
|
||||
target: "/sys/tables/info?tables=sys_user,sys_post",
|
||||
want: []string{"sys_user", "sys_post"},
|
||||
}, {
|
||||
name: "from a JSON body, as go-admin-ui v3.2.0 sends it",
|
||||
target: "/sys/tables/info",
|
||||
contentType: "application/json",
|
||||
body: `{"tables":"sys_user,sys_post"}`,
|
||||
want: []string{"sys_user", "sys_post"},
|
||||
}, {
|
||||
name: "the query wins when a request carries both",
|
||||
target: "/sys/tables/info?tables=sys_user",
|
||||
contentType: "application/json",
|
||||
body: `{"tables":"sys_post"}`,
|
||||
want: []string{"sys_user"},
|
||||
}, {
|
||||
name: "blank entries are dropped rather than imported as a nameless table",
|
||||
target: "/sys/tables/info?tables=sys_user,,%20,sys_post",
|
||||
want: []string{"sys_user", "sys_post"},
|
||||
}, {
|
||||
name: "a body carrying an empty list is an error",
|
||||
target: "/sys/tables/info",
|
||||
contentType: "application/json",
|
||||
body: `{"tables":""}`,
|
||||
wantErr: true,
|
||||
}, {
|
||||
name: "a body that is not JSON at all is an error, not a panic",
|
||||
target: "/sys/tables/info",
|
||||
contentType: "application/json",
|
||||
body: "sys_user",
|
||||
wantErr: true,
|
||||
}}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = newImportRequest(tc.target, tc.contentType, tc.body)
|
||||
|
||||
got, err := tablesToImport(c)
|
||||
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error, got %q", got)
|
||||
}
|
||||
if err.Error() != emptyTableNameMsg {
|
||||
t.Fatalf("message should be the one the front end shows, got %q", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if strings.Join(got, ",") != strings.Join(tc.want, ",") {
|
||||
t.Fatalf("got %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// insertMsg runs one import through the wired handler. It asserts nothing about
|
||||
// the import succeeding -- it cannot, over sqlite -- only about how far the
|
||||
// request got, which the empty-list message is what distinguishes.
|
||||
func insertMsg(t *testing.T, target, contentType, body string) bodyOf {
|
||||
t.Helper()
|
||||
return serveJSON(t,
|
||||
newEngine(t, http.MethodPost, "/sys/tables/info", SysTable{}.Insert),
|
||||
newImportRequest(target, contentType, body))
|
||||
}
|
||||
|
||||
func TestInsert_ReadsTheTableListFromEitherPlace(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
target string
|
||||
contentType string
|
||||
body string
|
||||
}{
|
||||
{"query", "/sys/tables/info?tables=sys_user", "", ""},
|
||||
{"JSON body", "/sys/tables/info", "application/json", `{"tables":"sys_user"}`},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := insertMsg(t, tc.target, tc.contentType, tc.body); got.Msg == emptyTableNameMsg {
|
||||
t.Fatalf("request carried a table name and was still rejected as empty: %+v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsert_RejectsAMissingTableList(t *testing.T) {
|
||||
if got := insertMsg(t, "/sys/tables/info", "", ""); got.Msg != emptyTableNameMsg {
|
||||
t.Fatalf("missing table list should be rejected, got %+v", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/app/other/models/tools"
|
||||
)
|
||||
|
||||
// jsonFieldPattern accepts any legal JS/TS identifier that starts with a
|
||||
// lowercase letter - not businessName's rule.
|
||||
//
|
||||
// This used to be businessName's own pattern (^[a-z][A-Za-z]+$, requiring at
|
||||
// least two letters and no digits), copied over on the theory that jsonField
|
||||
// "should tighten to the same identifier shape". That theory does not hold:
|
||||
// businessName is typed by a person on genInfoForm.vue, so a strict pattern
|
||||
// is a reasonable guardrail on human input. jsonField is computed by the
|
||||
// importer from the column name (sys_tables.go's namelist/JsonField loop) -
|
||||
// nobody types it, so the same pattern only rejects names the importer
|
||||
// legitimately produces. A one-letter column ("x") or a column ending in a
|
||||
// digit ("address2", "a1") both import to a single camelCase word with no
|
||||
// separators to re-capitalize, and both used to fail this check - meaning a
|
||||
// table that merely contained such a column could never save any config
|
||||
// again, unrelated columns included, since this check runs over every
|
||||
// column on every Update.
|
||||
//
|
||||
// What still has to be rejected is a jsonField that cannot be a raw object
|
||||
// key at all: empty, containing whitespace/punctuation, or leading with a
|
||||
// digit (`2faEnabled: 1` is not valid JS - identifiers cannot start with a
|
||||
// digit, and this is what lands as the property name in gen.go's generated
|
||||
// interface / lang file, both unquoted). Hence still anchoring on a
|
||||
// lowercase letter first, but no longer requiring a second character or
|
||||
// forbidding digits after it.
|
||||
var jsonFieldPattern = regexp.MustCompile(`^[a-z][A-Za-z0-9]*$`)
|
||||
|
||||
// colWidthMin/colWidthMax are API契约.md §2.1's suggested range for colWidth.
|
||||
const (
|
||||
colWidthMin = 40
|
||||
colWidthMax = 800
|
||||
)
|
||||
|
||||
// expressionMarkers flags the "meant to be evaluated" shapes API契约.md §2.1
|
||||
// says defaultValue must not carry: it is spliced into the generated
|
||||
// defaultModel() as a literal and never evaluated, so anything that looks
|
||||
// like a function call or a block is rejected outright rather than
|
||||
// generating code that silently does nothing.
|
||||
var expressionMarkers = []string{"(", ")", "{", "}", "`", ";", "=>"}
|
||||
|
||||
// validateAndSanitizeColumns enforces PRD 010 F10 on the columns carried by
|
||||
// a table update (sys_tables.go:357's Update handler, the one bind-and-save
|
||||
// path with no field-level validation at all - see API契约.md §1.2/§2.1,
|
||||
// decision D6).
|
||||
//
|
||||
// jsonField and defaultValue problems reject the request outright: letting
|
||||
// either through would corrupt the generated i18n file silently (a
|
||||
// duplicate or malformed jsonField becomes a duplicate or invalid key in
|
||||
// gen/{PackageName}/{BusinessName}.ts, see the lang-zh/lang-en templates).
|
||||
// An out-of-range colWidth does not reject - §2.1 says it "falls back to
|
||||
// the inferred value", so this resets it to the 0 sentinel in place and lets
|
||||
// R2's inference take over, the same as if the field had never been set.
|
||||
func validateAndSanitizeColumns(columns []tools.SysColumns) error {
|
||||
seen := make(map[string]bool, len(columns))
|
||||
for i := range columns {
|
||||
col := &columns[i]
|
||||
|
||||
if !jsonFieldPattern.MatchString(col.JsonField) {
|
||||
return fmt.Errorf("jsonField 格式不合法:%q,须以小写字母开头且只能包含英文字母", col.JsonField)
|
||||
}
|
||||
if seen[col.JsonField] {
|
||||
return fmt.Errorf("jsonField 在同一张表内重复:%q", col.JsonField)
|
||||
}
|
||||
seen[col.JsonField] = true
|
||||
|
||||
if col.ColWidth != 0 && (col.ColWidth < colWidthMin || col.ColWidth > colWidthMax) {
|
||||
col.ColWidth = 0
|
||||
}
|
||||
|
||||
for _, marker := range expressionMarkers {
|
||||
if strings.Contains(col.DefaultValue, marker) {
|
||||
return fmt.Errorf("defaultValue 不允许包含表达式或函数调用内容:%q", col.DefaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateBusinessNameUnique enforces PRD 010 F10's other half: two tables
|
||||
// sharing (packageName, businessName) write the same generated language
|
||||
// pack path, gen/{PackageName}/{BusinessName}.ts (see gen.go's
|
||||
// NOActionsGen), so the second one silently overwrites the first's
|
||||
// translations. tableID excludes the row being saved, so a table updating
|
||||
// its own unchanged name does not trip the check on itself.
|
||||
//
|
||||
// G10's other concern - colliding with the built-in admin/* i18n namespace -
|
||||
// does not apply here anymore: D9 moved generated keys to their own gen/
|
||||
// namespace, so this only has to guard generated tables against each other.
|
||||
func validateBusinessNameUnique(db *gorm.DB, packageName, businessName string, tableID int) error {
|
||||
var count int64
|
||||
err := db.Table("sys_tables").
|
||||
Where("package_name = ? AND business_name = ? AND table_id != ?", packageName, businessName, tableID).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return fmt.Errorf("packageName=%q 下 businessName=%q 已被其它表使用", packageName, businessName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/app/other/models/tools"
|
||||
)
|
||||
|
||||
func TestValidateAndSanitizeColumns_JsonFieldFormat(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
jsonField string
|
||||
wantErr bool
|
||||
}{
|
||||
{"lower camelCase", "userName", false},
|
||||
{"two-letter lowercase", "id", false},
|
||||
// The importer's own output (sys_tables.go's namelist/JsonField
|
||||
// loop), not made up: a single-letter column ("x"), and a column
|
||||
// whose last name segment ends in a digit ("address2", "a1") both
|
||||
// produce a jsonField with no separator left to re-capitalize.
|
||||
// These three used to be rejected - the whole point of this fix.
|
||||
{"single letter, real importer output for a column named x", "x", false},
|
||||
{"letters then a trailing digit, real importer output for address2", "address2", false},
|
||||
{"two letters then a digit, real importer output for a1", "a1", false},
|
||||
{"leading underscore rejected", "_id", true},
|
||||
{"leading digit rejected (not a legal identifier start)", "1name", true},
|
||||
{"snake_case rejected (importer never emits an underscore)", "user_name", true},
|
||||
{"dot rejected, would break the gen/{pkg}/{biz}.ts key path", "user.name", true},
|
||||
{"empty rejected", "", true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := validateAndSanitizeColumns([]tools.SysColumns{{JsonField: tc.jsonField}})
|
||||
if tc.wantErr && err == nil {
|
||||
t.Errorf("jsonField %q: want error, got nil", tc.jsonField)
|
||||
}
|
||||
if !tc.wantErr && err != nil {
|
||||
t.Errorf("jsonField %q: want no error, got %v", tc.jsonField, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAndSanitizeColumns_JsonFieldUniqueWithinTable(t *testing.T) {
|
||||
err := validateAndSanitizeColumns([]tools.SysColumns{
|
||||
{JsonField: "name"},
|
||||
{JsonField: "name"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("want error for a jsonField repeated in the same table, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAndSanitizeColumns_ColWidthOutOfRangeIsSanitizedNotRejected(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
width int
|
||||
want int
|
||||
}{
|
||||
{"zero (unconfigured) is left alone", 0, 0},
|
||||
{"in range is left alone", 150, 150},
|
||||
{"lower bound is left alone", colWidthMin, colWidthMin},
|
||||
{"upper bound is left alone", colWidthMax, colWidthMax},
|
||||
{"too small falls back to the sentinel", colWidthMin - 1, 0},
|
||||
{"too large falls back to the sentinel", colWidthMax + 1, 0},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cols := []tools.SysColumns{{JsonField: "name", ColWidth: tc.width}}
|
||||
if err := validateAndSanitizeColumns(cols); err != nil {
|
||||
t.Fatalf("colWidth %d: want no error (out-of-range sanitizes, it does not reject), got %v", tc.width, err)
|
||||
}
|
||||
if cols[0].ColWidth != tc.want {
|
||||
t.Errorf("colWidth %d: want sanitized to %d, got %d", tc.width, tc.want, cols[0].ColWidth)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAndSanitizeColumns_DefaultValueExpressionRejected(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
defaultValue string
|
||||
wantErr bool
|
||||
}{
|
||||
{"plain literal", "0", false},
|
||||
{"plain string literal", "active", false},
|
||||
{"empty (unconfigured)", "", false},
|
||||
{"function call rejected", "Date.now()", true},
|
||||
{"template literal rejected", "`x`", true},
|
||||
{"arrow function rejected", "() => 1", true},
|
||||
{"statement separator rejected", "1; drop", true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := validateAndSanitizeColumns([]tools.SysColumns{{JsonField: "name", DefaultValue: tc.defaultValue}})
|
||||
if tc.wantErr && err == nil {
|
||||
t.Errorf("defaultValue %q: want error, got nil", tc.defaultValue)
|
||||
}
|
||||
if !tc.wantErr && err != nil {
|
||||
t.Errorf("defaultValue %q: want no error, got %v", tc.defaultValue, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newBusinessNameTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(new(tools.SysTables)); err != nil {
|
||||
t.Fatalf("migrate sys_tables: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestValidateBusinessNameUnique(t *testing.T) {
|
||||
db := newBusinessNameTestDB(t)
|
||||
|
||||
existing := tools.SysTables{TBName: "sys_widget", PackageName: "biz", BusinessName: "widget"}
|
||||
if err := db.Table("sys_tables").Create(&existing).Error; err != nil {
|
||||
t.Fatalf("seed: %v", err)
|
||||
}
|
||||
|
||||
t.Run("same package, same businessName, different table: rejected", func(t *testing.T) {
|
||||
other := tools.SysTables{TBName: "sys_widget_copy", PackageName: "biz", BusinessName: "widget"}
|
||||
if err := db.Table("sys_tables").Create(&other).Error; err != nil {
|
||||
t.Fatalf("seed second row: %v", err)
|
||||
}
|
||||
// Unscoped: a plain Delete only soft-deletes (SysTables carries
|
||||
// common.ModelTime), which would leave this row's businessName
|
||||
// looking taken for the next subtest - production's own delete path
|
||||
// (SysTables.BatchDelete) hard-deletes for the same reason.
|
||||
defer db.Table("sys_tables").Unscoped().Delete(&other)
|
||||
|
||||
if err := validateBusinessNameUnique(db, "biz", "widget", other.TableId); err == nil {
|
||||
t.Error("want error for a businessName already used by another table in the same package, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("different package, same businessName: allowed", func(t *testing.T) {
|
||||
if err := validateBusinessNameUnique(db, "other-pkg", "widget", 0); err != nil {
|
||||
t.Errorf("want no error across different packages, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a table checking against its own current name: allowed", func(t *testing.T) {
|
||||
if err := validateBusinessNameUnique(db, "biz", "widget", existing.TableId); err != nil {
|
||||
t.Errorf("want no error when the only match is the row being saved itself, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -45,6 +45,19 @@ type SysColumns struct {
|
||||
CreateBy int `gorm:"column:create_by;size:20;" json:"createBy"`
|
||||
UpdateBy int `gorm:"column:update_By;size:20;" json:"updateBy"`
|
||||
|
||||
// ColWidth and DefaultValue back PRD 010 F1/F2 (代码生成器前端模板迁移 Vue 3).
|
||||
// Both use a sentinel default (0 / "") rather than NULL - see
|
||||
// docs-prd/010-代码生成器前端模板迁移Vue3/数据库变更.md §1.1: a non-pointer
|
||||
// int/string field can never read NULL back out, and NULL would give
|
||||
// "unconfigured" two representations instead of one. Callers test
|
||||
// ColWidth == 0 / DefaultValue == "" to detect "not configured".
|
||||
//
|
||||
// ColWidth deliberately has no gorm size tag: this codebase's "size:N"
|
||||
// convention on numeric fields maps to a narrow SQL integer type (see
|
||||
// column_width_test.go), and col_width needs to hold values up to 800.
|
||||
ColWidth int `gorm:"column:col_width;not null;default:0;comment:table column width in px, 0 = not configured" json:"colWidth"`
|
||||
DefaultValue string `gorm:"column:default_value;size:255;not null;default:'';comment:form field default value, empty = not configured" json:"defaultValue"`
|
||||
|
||||
common.ModelTime
|
||||
}
|
||||
|
||||
@@ -97,5 +110,23 @@ func (e *SysColumns) Update(tx *gorm.DB) (update SysColumns, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Updates(&e) above skips zero-value fields (GORM's struct-form Updates
|
||||
// always does), but ColWidth/DefaultValue's own "unconfigured" sentinel
|
||||
// is 0/"" (see the field comments on SysColumns) - so clearing either one
|
||||
// back to its sentinel is indistinguishable, to a struct-form Updates,
|
||||
// from "the caller didn't touch this field" and silently does not get
|
||||
// written. A map-form Updates does not skip zero values, so it is used
|
||||
// here for just these two columns rather than widening this to
|
||||
// Select("*") (which would also start writing every other zero-valued
|
||||
// field on this struct - Sort, the Pk/Required/... bools - and that is a
|
||||
// pre-existing gap in this method affecting fields outside PRD 010's
|
||||
// scope, not fixed here).
|
||||
if err = tx.Table("sys_columns").Model(&update).Updates(map[string]interface{}{
|
||||
"col_width": e.ColWidth,
|
||||
"default_value": e.DefaultValue,
|
||||
}).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GORM's Updates(struct) skips zero-value fields, and PRD 010 F1/F2 chose 0 /
|
||||
// "" as the sentinel for "unconfigured" (docs-prd/010-代码生成器前端模板迁移Vue3/
|
||||
// 数据库变更.md §1.1). Put those together and Update can set ColWidth/
|
||||
// DefaultValue but never clear them back to the sentinel: the struct-form
|
||||
// Updates call silently drops the very values this feature needs to write.
|
||||
func TestSysColumnsUpdateClearsSentinelFields(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(new(SysColumns)); err != nil {
|
||||
t.Fatalf("migrate sys_columns: %v", err)
|
||||
}
|
||||
|
||||
col := SysColumns{TableId: 1, ColumnName: "status", ColWidth: 150, DefaultValue: "active"}
|
||||
if _, err := col.Create(db); err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
|
||||
// Reset back to the sentinel - the UI action for "go back to inferred
|
||||
// width / no default", not merely "never configured".
|
||||
update := SysColumns{ColumnId: col.ColumnId, ColWidth: 0, DefaultValue: ""}
|
||||
if _, err := update.Update(db); err != nil {
|
||||
t.Fatalf("update: %v", err)
|
||||
}
|
||||
|
||||
var got SysColumns
|
||||
if err := db.Table("sys_columns").First(&got, col.ColumnId).Error; err != nil {
|
||||
t.Fatalf("read back: %v", err)
|
||||
}
|
||||
if got.ColWidth != 0 {
|
||||
t.Errorf("colWidth: want 0 (cleared), got %d - Update() did not write the sentinel back", got.ColWidth)
|
||||
}
|
||||
if got.DefaultValue != "" {
|
||||
t.Errorf("defaultValue: want \"\" (cleared), got %q - Update() did not write the sentinel back", got.DefaultValue)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -5,4 +5,4 @@ import "go-admin/app/demo/router"
|
||||
func init() {
|
||||
//注册路由 fixme 其他应用的路由,在本目录新建文件放在init方法
|
||||
AppRouters = append(AppRouters, router.InitRouter)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ func init() {
|
||||
rootCmd.AddCommand(app.StartCmd)
|
||||
}
|
||||
|
||||
//Execute : apply commands
|
||||
// Execute : apply commands
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(-1)
|
||||
|
||||
@@ -13,4 +13,4 @@ type SysApi struct {
|
||||
|
||||
func (SysApi) TableName() string {
|
||||
return "sys_api"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
package models
|
||||
|
||||
type SysMenu struct {
|
||||
MenuId int `json:"menuId" gorm:"primaryKey;autoIncrement"`
|
||||
MenuName string `json:"menuName" gorm:"size:128;"`
|
||||
Title string `json:"title" gorm:"size:128;"`
|
||||
Icon string `json:"icon" gorm:"size:128;"`
|
||||
Path string `json:"path" gorm:"size:128;"`
|
||||
Paths string `json:"paths" gorm:"size:128;"`
|
||||
MenuType string `json:"menuType" gorm:"size:1;"`
|
||||
Action string `json:"action" gorm:"size:16;"`
|
||||
Permission string `json:"permission" gorm:"size:255;"`
|
||||
ParentId int `json:"parentId" gorm:"size:11;"`
|
||||
NoCache bool `json:"noCache" gorm:"size:8;"`
|
||||
Breadcrumb string `json:"breadcrumb" gorm:"size:255;"`
|
||||
Component string `json:"component" gorm:"size:255;"`
|
||||
Sort int `json:"sort" gorm:"size:4;"`
|
||||
Visible string `json:"visible" gorm:"size:1;"`
|
||||
IsFrame string `json:"isFrame" gorm:"size:1;DEFAULT:0;"`
|
||||
SysApi []SysApi `json:"sysApi" gorm:"many2many:sys_menu_api_rule"`
|
||||
MenuId int `json:"menuId" gorm:"primaryKey;autoIncrement"`
|
||||
MenuName string `json:"menuName" gorm:"size:128;"`
|
||||
Title string `json:"title" gorm:"size:128;"`
|
||||
Icon string `json:"icon" gorm:"size:128;"`
|
||||
Path string `json:"path" gorm:"size:128;"`
|
||||
Paths string `json:"paths" gorm:"size:128;"`
|
||||
MenuType string `json:"menuType" gorm:"size:1;"`
|
||||
Action string `json:"action" gorm:"size:16;"`
|
||||
Permission string `json:"permission" gorm:"size:255;"`
|
||||
ParentId int `json:"parentId" gorm:"size:11;"`
|
||||
NoCache bool `json:"noCache" gorm:"size:8;"`
|
||||
Breadcrumb string `json:"breadcrumb" gorm:"size:255;"`
|
||||
Component string `json:"component" gorm:"size:255;"`
|
||||
Sort int `json:"sort" gorm:"size:4;"`
|
||||
Visible string `json:"visible" gorm:"size:1;"`
|
||||
IsFrame string `json:"isFrame" gorm:"size:1;DEFAULT:0;"`
|
||||
SysApi []SysApi `json:"sysApi" gorm:"many2many:sys_menu_api_rule"`
|
||||
ControlBy
|
||||
ModelTime
|
||||
}
|
||||
|
||||
func (SysMenu) TableName() string {
|
||||
return "sys_menu"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@ type SysPost struct {
|
||||
|
||||
func (SysPost) TableName() string {
|
||||
return "sys_post"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,4 +17,4 @@ type SysRole struct {
|
||||
|
||||
func (SysRole) TableName() string {
|
||||
return "sys_role"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/app/other/models/tools"
|
||||
"go-admin/cmd/migrate/migration"
|
||||
common "go-admin/common/models"
|
||||
)
|
||||
|
||||
// Add sys_columns.col_width and sys_columns.default_value for PRD 010 F1/F2
|
||||
// (代码生成器前端模板迁移 Vue 3).
|
||||
//
|
||||
// col_width backs R2's column-width inference fallback and default_value
|
||||
// backs R1/A6's "unconfigured rows still generate a usable page" guarantee -
|
||||
// see docs-prd/010-代码生成器前端模板迁移Vue3/数据库变更.md §1.1 for why both
|
||||
// defaults are sentinels (0 / "") rather than NULL: a non-pointer Go int/
|
||||
// string field can never read NULL back out, and NULL would give
|
||||
// "unconfigured" two representations instead of one.
|
||||
//
|
||||
// Ordered after 1786700003000, so this reads tools.SysColumns (the runtime
|
||||
// model sys_columns's Update/GetPage/GetSysTablesInfo actually query through)
|
||||
// rather than cmd/migrate/migration/models, matching every migration in this
|
||||
// directory since sys_columns was converted - see
|
||||
// 1786700004000_generator_tables_marker.go and schema_coverage_test.go's
|
||||
// TestPostConversionMigrationsAvoidFrozenSeedModels.
|
||||
//
|
||||
// Hard prerequisite: tools.SysColumns must already declare ColWidth and
|
||||
// DefaultValue (with the gorm tags in the doc above) by the time this file
|
||||
// is compiled - AddColumn reads the column definition off the struct's own
|
||||
// tag, not off anything in this file. Landing this migration without that
|
||||
// model change first makes HasColumn/AddColumn silently do nothing (the
|
||||
// field lookup fails and AddColumn returns an error naming the missing
|
||||
// field), which fails loudly rather than silently - see the "no such field"
|
||||
// error - so this is caught at migrate time, not left for a report later.
|
||||
func init() {
|
||||
_, fileName, _, _ := runtime.Caller(0)
|
||||
migration.Migrate.SetVersion(migration.GetFilename(fileName), _1786700010000GenColumnLayoutFields)
|
||||
}
|
||||
|
||||
func _1786700010000GenColumnLayoutFields(db *gorm.DB, version string) error {
|
||||
m := db.Migrator()
|
||||
if !m.HasColumn(&tools.SysColumns{}, "ColWidth") {
|
||||
if err := m.AddColumn(&tools.SysColumns{}, "ColWidth"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !m.HasColumn(&tools.SysColumns{}, "DefaultValue") {
|
||||
if err := m.AddColumn(&tools.SysColumns{}, "DefaultValue"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return db.Create(&common.Migration{Version: version}).Error
|
||||
}
|
||||
@@ -45,8 +45,8 @@ func (e *QiNiuKODO) getToken() (string, error) {
|
||||
return putPolicy.UploadToken(mac), nil
|
||||
}
|
||||
|
||||
//Setup 装载
|
||||
//endpoint sss
|
||||
// Setup 装载
|
||||
// endpoint sss
|
||||
func (e *QiNiuKODO) Setup(endpoint, accessKeyID, accessKeySecret, BucketName string, options ...ClientOption) error {
|
||||
|
||||
mac := qbox.NewMac(accessKeyID, accessKeySecret)
|
||||
|
||||
@@ -10,8 +10,8 @@ type ALiYunOSS struct {
|
||||
BucketName string
|
||||
}
|
||||
|
||||
//Setup 装载
|
||||
//endpoint sss
|
||||
// Setup 装载
|
||||
// endpoint sss
|
||||
func (e *ALiYunOSS) Setup(endpoint, accessKeyID, accessKeySecret, BucketName string, options ...ClientOption) error {
|
||||
client, err := oss.New(endpoint, accessKeyID, accessKeySecret)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
// NoCache is a middleware function that appends headers
|
||||
// to prevent the client from caching the HTTP response.
|
||||
func NoCache(c *gin.Context) {
|
||||
c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
|
||||
c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate")
|
||||
c.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
|
||||
c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
|
||||
c.Next()
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestNoCache(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
NoCache(c)
|
||||
|
||||
if got := w.Header().Get("Cache-Control"); got != "no-cache, no-store, max-age=0, must-revalidate" {
|
||||
t.Errorf("Cache-Control = %q", got)
|
||||
}
|
||||
if got := w.Header().Get("Expires"); got != "Thu, 01 Jan 1970 00:00:00 GMT" {
|
||||
t.Errorf("Expires = %q", got)
|
||||
}
|
||||
if got := w.Header().Get("Last-Modified"); got == "" {
|
||||
t.Error("Last-Modified should not be empty")
|
||||
} else if _, err := time.Parse(http.TimeFormat, got); err != nil {
|
||||
t.Errorf("Last-Modified = %q is not a valid HTTP time: %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
t.Run("OPTIONS request", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodOptions, "/", nil)
|
||||
|
||||
Options(c)
|
||||
|
||||
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "*" {
|
||||
t.Errorf("Access-Control-Allow-Origin = %q", got)
|
||||
}
|
||||
if got := w.Header().Get("Access-Control-Allow-Methods"); got != "GET,POST,PUT,PATCH,DELETE,OPTIONS" {
|
||||
t.Errorf("Access-Control-Allow-Methods = %q", got)
|
||||
}
|
||||
if got := w.Header().Get("Access-Control-Allow-Headers"); got != "authorization, origin, content-type, accept" {
|
||||
t.Errorf("Access-Control-Allow-Headers = %q", got)
|
||||
}
|
||||
if got := w.Header().Get("Allow"); got != "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS" {
|
||||
t.Errorf("Allow = %q", got)
|
||||
}
|
||||
if got := w.Header().Get("Content-Type"); got != "application/json" {
|
||||
t.Errorf("Content-Type = %q", got)
|
||||
}
|
||||
if !c.IsAborted() {
|
||||
t.Error("expected the request to be aborted")
|
||||
}
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("status code = %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-OPTIONS request", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
Options(c)
|
||||
|
||||
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Errorf("Access-Control-Allow-Origin = %q, want empty", got)
|
||||
}
|
||||
if c.IsAborted() {
|
||||
t.Error("expected the request not to be aborted")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSecure(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
t.Run("without TLS", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
Secure(c)
|
||||
|
||||
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "*" {
|
||||
t.Errorf("Access-Control-Allow-Origin = %q", got)
|
||||
}
|
||||
if got := w.Header().Get("X-Content-Type-Options"); got != "nosniff" {
|
||||
t.Errorf("X-Content-Type-Options = %q", got)
|
||||
}
|
||||
if got := w.Header().Get("X-XSS-Protection"); got != "1; mode=block" {
|
||||
t.Errorf("X-XSS-Protection = %q", got)
|
||||
}
|
||||
if got := w.Header().Get("Strict-Transport-Security"); got != "" {
|
||||
t.Errorf("Strict-Transport-Security = %q, want empty without TLS", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with TLS", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
c.Request.TLS = &tls.ConnectionState{}
|
||||
|
||||
Secure(c)
|
||||
|
||||
if got := w.Header().Get("Strict-Transport-Security"); got != "max-age=31536000" {
|
||||
t.Errorf("Strict-Transport-Security = %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -38,6 +38,6 @@ var CasbinExclude = []UrlInfo{
|
||||
{Url: "/", Method: "GET"},
|
||||
{Url: "/api/v1/server-monitor", Method: "GET"},
|
||||
{Url: "/api/v1/public/uploadFile", Method: "POST"},
|
||||
{Url: "/api/v1/user/pwd/set", Method: "PUT"},
|
||||
{Url: "/api/v1/user/pwd/set", Method: "PUT"},
|
||||
{Url: "/api/v1/sys-user", Method: "PUT"},
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询{{.ClassName}}列表
|
||||
export function list{{.ClassName}}(query) {
|
||||
return request({
|
||||
url: '/api/v1/{{.ModuleName}}',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询{{.ClassName}}详细
|
||||
export function get{{.ClassName}} ({{.PkJsonField}}) {
|
||||
return request({
|
||||
url: '/api/v1/{{.ModuleName}}/' + {{.PkJsonField}},
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 新增{{.ClassName}}
|
||||
export function add{{.ClassName}}(data) {
|
||||
return request({
|
||||
url: '/api/v1/{{.ModuleName}}',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改{{.ClassName}}
|
||||
export function update{{.ClassName}}(data) {
|
||||
return request({
|
||||
url: '/api/v1/{{.ModuleName}}/'+data.{{.PkJsonField}},
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除{{.ClassName}}
|
||||
export function del{{.ClassName}}(data) {
|
||||
return request({
|
||||
url: '/api/v1/{{.ModuleName}}',
|
||||
method: 'delete',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
{{- range $i, $col := .Columns}}
|
||||
{{- if $i}},{{end}}
|
||||
{{$col.JsonField}}: {{if $col.ColumnComment}}{{singleQuote $col.ColumnComment}}{{else}}{{singleQuote $col.JsonField}}{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
{{- range $i, $col := .Columns}}
|
||||
{{- if $i}},{{end}}
|
||||
{{$col.JsonField}}: {{if $col.ColumnComment}}{{singleQuote $col.ColumnComment}}{{else}}{{singleQuote $col.JsonField}}{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
{{- /*
|
||||
$pkType: the primary key's TS type for get{ClassName}'s parameter.
|
||||
Defaults to "number" - true for every column but string primary keys
|
||||
(natural keys), which do exist (sys_tables.go:323-338 gives a primary
|
||||
key column GoType "string" whenever its ColumnType is not int-shaped).
|
||||
Matches vue.go.template's own $pkType derivation exactly (F4) - useForm
|
||||
there is typed on the same column, and a mismatch between the two is a
|
||||
TS compile error at the call site, not a runtime bug.
|
||||
*/ -}}
|
||||
{{- $pkType := "number" -}}
|
||||
{{- $hasQuery := false -}}
|
||||
{{- range .Columns -}}
|
||||
{{- if and .Pk (eq .GoType "string") }}{{$pkType = "string"}}{{end -}}
|
||||
{{- if eq .IsQuery "1" }}{{$hasQuery = true}}{{end -}}
|
||||
{{- end -}}
|
||||
import request from '@/utils/request'
|
||||
import type { ApiResponse, PageQuery, PageResult, Id } from '@/types/api'
|
||||
|
||||
export interface {{.ClassName}} {
|
||||
{{- range .Columns}}
|
||||
{{.JsonField}}?: {{if eq .GoType "int" -}}
|
||||
number
|
||||
{{- else if eq .GoType "int64" -}}
|
||||
number
|
||||
{{- else if eq .GoType "float32" -}}
|
||||
number
|
||||
{{- else if eq .GoType "float64" -}}
|
||||
number
|
||||
{{- else -}}
|
||||
string
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
{{if $hasQuery -}}
|
||||
export interface {{.ClassName}}Query {
|
||||
{{- range .Columns}}
|
||||
{{- if eq .IsQuery "1"}}
|
||||
{{.JsonField}}?: {{if eq .GoType "int" -}}
|
||||
number
|
||||
{{- else if eq .GoType "int64" -}}
|
||||
number
|
||||
{{- else if eq .GoType "float32" -}}
|
||||
number
|
||||
{{- else if eq .GoType "float64" -}}
|
||||
number
|
||||
{{- else -}}
|
||||
string
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
{{- else -}}
|
||||
{{- /*
|
||||
No column is marked IsQuery - a plain display table with no search form
|
||||
is a normal shape, not an edge case, so this still has to produce a type
|
||||
useTable<Row, Query>/list{ClassName}(query: Query & PageQuery) can use.
|
||||
`export interface {ClassName}Query {}` is what naturally falls out of the
|
||||
range above finding nothing to iterate, but an empty interface trips
|
||||
@typescript-eslint/no-empty-object-type and fails pnpm lint.
|
||||
|
||||
Record<string, never> (this file's first attempt, and the type
|
||||
useTable.ts's own `TQuery extends object = Record<string, never>` default
|
||||
uses) looks like the obvious match but is wrong here: it is a mapped type
|
||||
over *every* string key, each mapped to never, so intersecting it with
|
||||
PageQuery does not leave PageQuery alone - `pageIndex` becomes
|
||||
`never & number`, i.e. never, and no value can be passed for it at all.
|
||||
useTable.ts itself never hits this because its one internal use of
|
||||
`TQuery & PageQuery` goes through an `as` cast rather than a structural
|
||||
check (composables/useTable.ts ~line 160); code that builds the object
|
||||
literal directly - such as a foreign-key column's
|
||||
`list{FkClass}({ pageIndex: 1, pageSize: 100 })` call in vue.go.template -
|
||||
is not casting anything and hits the real error, only when the referenced
|
||||
table happens to have no query columns of its own (a plain lookup/dict
|
||||
table used as a dropdown source, not a rare shape).
|
||||
|
||||
Record<never, never> is the type with the same intent - "no query
|
||||
columns" - but the mapped-type domain is `never`, so it has no keys at
|
||||
all rather than "every key, mapped to never": it behaves as the empty
|
||||
object type `{}` under intersection, leaving PageQuery's own pageIndex/
|
||||
pageSize untouched, and confirmed separately not to trip
|
||||
no-empty-object-type either (it is a generic instantiation, not a
|
||||
literal `{}` type annotation).
|
||||
*/ -}}
|
||||
export type {{.ClassName}}Query = Record<never, never>
|
||||
{{- end}}
|
||||
|
||||
export function list{{.ClassName}}(query: {{.ClassName}}Query & PageQuery) {
|
||||
return request<ApiResponse<PageResult<{{.ClassName}}>>>({
|
||||
url: '/api/v1/{{.ModuleName}}',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function get{{.ClassName}}({{.PkJsonField}}: {{$pkType}}) {
|
||||
return request<ApiResponse<{{.ClassName}}>>({
|
||||
url: '/api/v1/{{.ModuleName}}/' + {{.PkJsonField}},
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function add{{.ClassName}}(data: {{.ClassName}}) {
|
||||
return request<ApiResponse<{{.ClassName}}>>({
|
||||
url: '/api/v1/{{.ModuleName}}',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function update{{.ClassName}}(data: {{.ClassName}}) {
|
||||
return request<ApiResponse<{{.ClassName}}>>({
|
||||
url: '/api/v1/{{.ModuleName}}/' + data.{{.PkJsonField}},
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function del{{.ClassName}}(ids: Id[]) {
|
||||
return request<ApiResponse<null>>({
|
||||
url: '/api/v1/{{.ModuleName}}',
|
||||
method: 'delete',
|
||||
data: { ids: ids.map(Number) }
|
||||
})
|
||||
}
|
||||
+377
-467
@@ -1,479 +1,389 @@
|
||||
{{$tableComment:=.TableComment}}
|
||||
{{- /*
|
||||
Vue 3 + Element Plus + TypeScript list page (PRD 010, F4).
|
||||
|
||||
Shape matches go-admin-ui/src/views/demo/product/index.vue, the reference
|
||||
page AGENTS.md names: PageContainer + ProTable + useTable/useForm/useRemove,
|
||||
<script setup lang="ts">. The old template produced slot-scope/.sync/.native
|
||||
syntax that Vue 3 removed outright (PRD 010 G1) -- this is not a patch on
|
||||
that file, it is a different template for a different framework version.
|
||||
|
||||
Every label goes through $t('gen.{PackageName}.{BusinessName}.{JsonField}'),
|
||||
never a literal ColumnComment -- see src/lang/{locale}/gen/index.ts (F9) for
|
||||
how that namespace is loaded. This is also why the file must not contain a
|
||||
literal CJK character anywhere, comments included: D10's acceptance check is
|
||||
a bare regex scan of the rendered output with no exception for "but this one
|
||||
is a comment", so a Chinese aside here would fail the same test a stray
|
||||
placeholder="{{"{{"}}.ColumnComment{{"}}"}}" would.
|
||||
|
||||
HtmlType has seven stored values (PRD 010 G8) and only four render here on
|
||||
purpose: checkbox and datetime became selectable in the F7 front-end change
|
||||
(editTable.vue), so they get a branch; file stays disabled there, but a row
|
||||
imported or edited before that change can still carry "file" or any other
|
||||
value this template does not know -- the final branch below renders those,
|
||||
and anything else future work introduces, as a plain input rather than
|
||||
emitting nothing (PRD 010 phase-3 constraint #1: a silently empty field is
|
||||
worse than a plain one).
|
||||
*/ -}}
|
||||
{{- $package := .PackageName -}}
|
||||
{{- $business := .BusinessName -}}
|
||||
{{- /*
|
||||
Whether any column needs a given import, computed once by walking .Columns
|
||||
rather than at each usage site -- text/template has no way to ask "did the
|
||||
loop below already import this", so the alternative is repeating the same
|
||||
import line once per matching column. "$var = value" (not ":=") reassigns an
|
||||
outer-scope variable from inside a range -- a text/template feature since
|
||||
Go 1.11, needed here because a range body cannot otherwise leave a mark on
|
||||
anything outside itself.
|
||||
|
||||
Each condition below must match, term for term, the condition guarding the
|
||||
markup or script that actually consumes the import -- not just "this column
|
||||
has a DictType/FkTableName", which is necessary but not sufficient. A column
|
||||
can carry dictionary or foreign-key metadata that no rendered branch reads:
|
||||
FkTableName/DictType lose to each other by priority (FK wins search, list
|
||||
and the form's select branch; the form's radio branch never looks at FK at
|
||||
all), and a column can carry either one while being neither queryable nor
|
||||
listed nor an insertable select/radio -- created_at/updated_at are exactly
|
||||
this: sys_tables.go assigns HtmlType "datetime" to any timestamp/datetime
|
||||
column on import whether or not it ever reaches IsList, because GetList's
|
||||
audit-column exclusion is a separate, later step. Get a term here wrong in
|
||||
either direction and either an import goes unused (no-unused-vars) or a real
|
||||
usage silently loses its import (a ReferenceError this template cannot see
|
||||
coming, since Vue components are the last stage that runs).
|
||||
*/ -}}
|
||||
{{- $hasDict := false -}}
|
||||
{{- $hasDictList := false -}}
|
||||
{{- $hasFk := false -}}
|
||||
{{- $hasDatetime := false -}}
|
||||
{{- $hasRules := false -}}
|
||||
{{- $hasQuery := false -}}
|
||||
{{- $pkType := "number" -}}
|
||||
{{- range .Columns -}}
|
||||
{{- $dictUsed := and (ne .DictType "") (or (and (eq .IsQuery "1") (eq .FkTableName "")) (and (eq .IsList "1") (eq .FkTableName "")) (and (eq .IsInsert "1") (eq .HtmlType "select") (eq .FkTableName "")) (and (eq .IsInsert "1") (eq .HtmlType "radio"))) -}}
|
||||
{{- $fkUsed := and (ne .FkTableName "") (or (eq .IsQuery "1") (eq .IsList "1") (and (eq .IsInsert "1") (eq .HtmlType "select"))) -}}
|
||||
{{- if $dictUsed }}{{$hasDict = true}}{{end -}}
|
||||
{{- if and (eq .IsList "1") (eq .FkTableName "") (ne .DictType "") }}{{$hasDictList = true}}{{end -}}
|
||||
{{- if $fkUsed }}{{$hasFk = true}}{{end -}}
|
||||
{{- if and (eq .IsList "1") (eq .FkTableName "") (eq .DictType "") (eq .HtmlType "datetime") }}{{$hasDatetime = true}}{{end -}}
|
||||
{{- if eq .IsQuery "1" }}{{$hasQuery = true}}{{end -}}
|
||||
{{- if and (eq .IsInsert "1") (eq .IsRequired "1") (not .Pk) (ne .GoField "CreatedAt") (ne .GoField "UpdatedAt") (ne .GoField "DeletedAt") (ne .GoField "UpdateBy") (ne .GoField "CreateBy") }}{{$hasRules = true}}{{end -}}
|
||||
{{- if and .Pk (eq .GoType "string") }}{{$pkType = "string"}}{{end -}}
|
||||
{{- end -}}
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<template #wrapper>
|
||||
<el-card class="box-card">
|
||||
<el-form ref="queryForm" :model="queryParams" :inline="true" label-width="68px">
|
||||
{{range .Columns}}
|
||||
{{- $x := .IsQuery -}}
|
||||
{{- if (eq $x "1") -}}
|
||||
<el-form-item label="{{.ColumnComment}}" prop="{{.JsonField}}">
|
||||
{{- if ne .FkTableName "" -}}
|
||||
<el-select v-model="queryParams.{{.JsonField}}"
|
||||
placeholder="请选择" clearable size="small" {{if eq .IsEdit "false" -}} :disabled="isEdit" {{- end }}>
|
||||
<el-option
|
||||
v-for="dict in {{.JsonField}}Options"
|
||||
:key="dict.key"
|
||||
:label="dict.value"
|
||||
:value="dict.key"
|
||||
/>
|
||||
</el-select>
|
||||
{{- else -}}
|
||||
{{if eq .DictType "" -}}
|
||||
<el-input v-model="queryParams.{{.JsonField}}" placeholder="请输入{{.ColumnComment}}" clearable
|
||||
size="small" @keyup.enter.native="handleQuery"/>
|
||||
{{- else -}}
|
||||
<el-select v-model="queryParams.{{.JsonField}}"
|
||||
placeholder="{{$tableComment}}{{.ColumnComment}}" clearable size="small">
|
||||
<el-option
|
||||
v-for="dict in {{.JsonField}}Options"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
</el-form-item>
|
||||
{{end}}
|
||||
{{- end }}
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<PageContainer>
|
||||
<ProTable :table="table" selection row-key="{{.PkJsonField}}">
|
||||
{{- if $hasQuery}}
|
||||
<template #search>
|
||||
{{- range .Columns}}
|
||||
{{- if eq .IsQuery "1"}}
|
||||
{{- $key := printf "gen.%s.%s.%s" $package $business .JsonField}}
|
||||
<el-form-item :label="$t('{{$key}}')">
|
||||
{{- if ne .FkTableName ""}}
|
||||
<el-select v-model="table.query.{{.JsonField}}" clearable :placeholder="$t('common.selectPlaceholder')">
|
||||
<el-option
|
||||
v-for="item in {{.JsonField}}FkOptions"
|
||||
:key="item.{{.FkLabelId}}"
|
||||
:label="item.{{.FkLabelName}}"
|
||||
:value="item.{{.FkLabelId}}"
|
||||
/>
|
||||
</el-select>
|
||||
{{- else if ne .DictType ""}}
|
||||
<el-select v-model="table.query.{{.JsonField}}" clearable :placeholder="$t('common.selectPlaceholder')">
|
||||
<el-option
|
||||
v-for="dict in {{.JsonField}}DictOptions"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
{{- else if eq .HtmlType "datetime"}}
|
||||
<el-date-picker
|
||||
v-model="table.query.{{.JsonField}}"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD[T]HH:mm:ssZ"
|
||||
clearable
|
||||
/>
|
||||
{{- else}}
|
||||
<el-input v-model="table.query.{{.JsonField}}" clearable />
|
||||
{{- end}}
|
||||
</el-form-item>
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
</template>
|
||||
{{- end}}
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-permisaction="['{{.PackageName}}:{{.BusinessName}}:add']"
|
||||
type="primary"
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
@click="handleAdd"
|
||||
>新增
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-permisaction="['{{.PackageName}}:{{.BusinessName}}:edit']"
|
||||
type="success"
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
:disabled="single"
|
||||
@click="handleUpdate"
|
||||
>修改
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-permisaction="['{{.PackageName}}:{{.BusinessName}}:remove']"
|
||||
type="danger"
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
:disabled="multiple"
|
||||
@click="handleDelete"
|
||||
>删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<template #toolbar>
|
||||
<el-button v-permisaction="['{{.PackageName}}:{{.BusinessName}}:add']" type="primary" @click="form.openCreate()">
|
||||
{{ "{{" }} $t('common.add') {{ "}}" }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-permisaction="['{{.PackageName}}:{{.BusinessName}}:remove']"
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="table.multiple"
|
||||
@click="remove(table.selectedIds)"
|
||||
>
|
||||
{{ "{{" }} $t('common.delete') {{ "}}" }}
|
||||
</el-button>
|
||||
</template>
|
||||
{{- range .Columns}}
|
||||
{{- if eq .IsList "1"}}
|
||||
{{- $key := printf "gen.%s.%s.%s" $package $business .JsonField}}
|
||||
{{- if ne .FkTableName ""}}
|
||||
|
||||
<el-table v-loading="loading" :data="{{.BusinessName}}List" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center"/>
|
||||
{{- range .Columns -}}
|
||||
{{- $x := .IsList -}}
|
||||
{{- if (eq $x "1") }}
|
||||
{{- if ne .FkTableName "" -}}
|
||||
<el-table-column label="{{.ColumnComment}}" align="center" prop="{{.JsonField}}" :formatter="{{.JsonField}}Format" width="100">
|
||||
<template slot-scope="scope">
|
||||
{{ "{{" }} {{.JsonField}}Format(scope.row) {{"}}"}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('{{$key}}')" min-width="{{if .ColWidth}}{{.ColWidth}}{{else}}120{{end}}" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ "{{" }} {{.JsonField}}Label(row.{{.JsonField}}) {{ "}}" }}</template>
|
||||
</el-table-column>
|
||||
{{- else if ne .DictType ""}}
|
||||
|
||||
{{- else -}}
|
||||
{{- if ne .DictType "" -}}
|
||||
<el-table-column label="{{.ColumnComment}}" align="center" prop="{{.JsonField}}"
|
||||
:formatter="{{.JsonField}}Format" width="100">
|
||||
<template slot-scope="scope">
|
||||
{{ "{{" }} {{.JsonField}}Format(scope.row) {{"}}"}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('{{$key}}')" min-width="{{if .ColWidth}}{{.ColWidth}}{{else}}120{{end}}">
|
||||
<template #default="{ row }">{{ "{{" }} dictLabel({{.JsonField}}DictOptions, row.{{.JsonField}}) {{ "}}" }}</template>
|
||||
</el-table-column>
|
||||
{{- else if eq .HtmlType "datetime"}}
|
||||
|
||||
{{- end -}}
|
||||
{{- if eq .DictType "" -}}
|
||||
{{- if eq .HtmlType "datetime" -}}
|
||||
<el-table-column label="{{.ColumnComment}}" align="center" prop="{{.JsonField}}"
|
||||
:show-overflow-tooltip="true">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ "{{" }} parseTime(scope.row.{{.JsonField}}) {{"}}"}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
{{- else -}}
|
||||
<el-table-column label="{{.ColumnComment}}" align="center" prop="{{.JsonField}}"
|
||||
:show-overflow-tooltip="true"/>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
slot="reference"
|
||||
v-permisaction="['{{.PackageName}}:{{.BusinessName}}:edit']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
>修改
|
||||
</el-button>
|
||||
<el-popconfirm
|
||||
class="delete-popconfirm"
|
||||
title="确认要删除吗?"
|
||||
confirm-button-text="删除"
|
||||
@confirm="handleDelete(scope.row)"
|
||||
>
|
||||
<el-button
|
||||
slot="reference"
|
||||
v-permisaction="['{{.PackageName}}:{{.BusinessName}}:remove']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
>删除
|
||||
</el-button>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table-column :label="$t('{{$key}}')" min-width="{{if .ColWidth}}{{.ColWidth}}{{else}}110{{end}}">
|
||||
<template #default="{ row }"><DateCell :value="row.{{.JsonField}}" /></template>
|
||||
</el-table-column>
|
||||
{{- else}}
|
||||
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageIndex"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('{{$key}}')"
|
||||
prop="{{.JsonField}}"
|
||||
min-width="{{if .ColWidth}}{{.ColWidth}}{{else}}120{{end}}"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
<!-- 添加或修改对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="500px">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||
{{ range .Columns }}
|
||||
{{- $x := .IsInsert -}}
|
||||
{{- if (eq $x "1") -}}
|
||||
{{- if (.Pk) }}
|
||||
{{- else if eq .GoField "CreatedAt" -}}
|
||||
{{- else if eq .GoField "UpdatedAt" -}}
|
||||
{{- else if eq .GoField "DeletedAt" -}}
|
||||
{{- else if eq .GoField "UpdateBy" -}}
|
||||
{{- else if eq .GoField "CreateBy" -}}
|
||||
{{- else }}
|
||||
<el-form-item label="{{.ColumnComment}}" prop="{{.JsonField}}">
|
||||
{{ if eq "input" .HtmlType -}}
|
||||
<el-input v-model{{if eq .GoType "int64" -}}.number{{- end}}="form.{{.JsonField}}" placeholder="{{.ColumnComment}}"
|
||||
{{if eq .IsEdit "false" -}}:disabled="isEdit" {{- end}}/>
|
||||
{{- else if eq "select" .HtmlType -}}
|
||||
{{- if ne .FkTableName "" -}}
|
||||
<el-select v-model="form.{{.JsonField}}"
|
||||
placeholder="请选择" {{if eq .IsEdit "false" -}} :disabled="isEdit" {{- end }}>
|
||||
<el-option
|
||||
v-for="dict in {{.JsonField}}Options"
|
||||
:key="dict.key"
|
||||
:label="dict.value"
|
||||
:value="dict.key"
|
||||
/>
|
||||
</el-select>
|
||||
{{- else -}}
|
||||
<el-select v-model="form.{{.JsonField}}"
|
||||
placeholder="请选择" {{if eq .IsEdit "false" -}} :disabled="isEdit" {{- end }}>
|
||||
<el-option
|
||||
v-for="dict in {{.JsonField}}Options"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
{{- end -}}
|
||||
{{- else if eq "radio" .HtmlType -}}
|
||||
<el-radio-group v-model="form.{{.JsonField}}">
|
||||
<el-radio
|
||||
v-for="dict in {{.JsonField}}Options"
|
||||
:key="dict.value"
|
||||
:label="dict.value"
|
||||
>{{"{{"}} dict.label {{"}}"}}</el-radio>
|
||||
</el-radio-group>
|
||||
{{- else if eq "file" .HtmlType -}}
|
||||
<el-input
|
||||
v-model="form.{{.JsonField}}"
|
||||
placeholder="图片"
|
||||
/>
|
||||
<el-button type="primary" @click="fileShow{{.GoField}}">选择文件</el-button>
|
||||
{{- else if eq "datetime" .HtmlType -}}
|
||||
<el-date-picker
|
||||
v-model="form.{{.JsonField}}"
|
||||
type="datetime"
|
||||
placeholder="选择日期">
|
||||
</el-date-picker>
|
||||
{{- else if eq "textarea" .HtmlType -}}
|
||||
<el-input
|
||||
v-model="form.{{.JsonField}}"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入内容">
|
||||
</el-input>
|
||||
{{- end }}
|
||||
</el-form-item>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
</BasicLayout>
|
||||
<template #actions="{ row }">
|
||||
<el-button v-permisaction="['{{.PackageName}}:{{.BusinessName}}:edit']" link type="primary" @click="form.openEdit(row)">
|
||||
{{ "{{" }} $t('common.edit') {{ "}}" }}
|
||||
</el-button>
|
||||
<el-button v-permisaction="['{{.PackageName}}:{{.BusinessName}}:remove']" link type="danger" @click="remove(row.{{.PkJsonField}})">
|
||||
{{ "{{" }} $t('common.delete') {{ "}}" }}
|
||||
</el-button>
|
||||
</template>
|
||||
</ProTable>
|
||||
|
||||
<el-dialog
|
||||
v-model="form.visible"
|
||||
:title="form.title"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="form.reset"
|
||||
>
|
||||
<el-form
|
||||
:ref="form.bindFormRef"
|
||||
v-loading="form.loading"
|
||||
:model="form.model"
|
||||
{{- if $hasRules}}
|
||||
:rules="form.rules"
|
||||
{{- end}}
|
||||
label-width="100px"
|
||||
>
|
||||
{{- range .Columns}}
|
||||
{{- if and (eq .IsInsert "1") (not .Pk) (ne .GoField "CreatedAt") (ne .GoField "UpdatedAt") (ne .GoField "DeletedAt") (ne .GoField "UpdateBy") (ne .GoField "CreateBy")}}
|
||||
{{- $key := printf "gen.%s.%s.%s" $package $business .JsonField}}
|
||||
<el-form-item :label="$t('{{$key}}')" prop="{{.JsonField}}">
|
||||
{{- if eq .HtmlType "select"}}
|
||||
{{- if ne .FkTableName ""}}
|
||||
<el-select v-model="form.model.{{.JsonField}}" :placeholder="$t('common.selectPlaceholder')">
|
||||
<el-option
|
||||
v-for="item in {{.JsonField}}FkOptions"
|
||||
:key="item.{{.FkLabelId}}"
|
||||
:label="item.{{.FkLabelName}}"
|
||||
:value="item.{{.FkLabelId}}"
|
||||
/>
|
||||
</el-select>
|
||||
{{- else if ne .DictType ""}}
|
||||
<el-select v-model="form.model.{{.JsonField}}" :placeholder="$t('common.selectPlaceholder')">
|
||||
<el-option
|
||||
v-for="dict in {{.JsonField}}DictOptions"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
{{- else}}
|
||||
<el-input v-model="form.model.{{.JsonField}}" />
|
||||
{{- end}}
|
||||
{{- else if eq .HtmlType "radio"}}
|
||||
{{- if ne .DictType ""}}
|
||||
<el-radio-group v-model="form.model.{{.JsonField}}">
|
||||
<el-radio v-for="dict in {{.JsonField}}DictOptions" :key="dict.value" :value="dict.value">
|
||||
{{ "{{" }} dict.label {{ "}}" }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
{{- else}}
|
||||
<el-input v-model="form.model.{{.JsonField}}" />
|
||||
{{- end}}
|
||||
{{- else if eq .HtmlType "checkbox"}}
|
||||
<el-checkbox v-model="form.model.{{.JsonField}}" true-value="1" false-value="0" />
|
||||
{{- else if eq .HtmlType "datetime"}}
|
||||
<el-date-picker
|
||||
v-model="form.model.{{.JsonField}}"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD[T]HH:mm:ssZ"
|
||||
/>
|
||||
{{- else if eq .HtmlType "textarea"}}
|
||||
<el-input v-model="form.model.{{.JsonField}}" type="textarea" :rows="2" />
|
||||
{{- else}}
|
||||
<el-input v-model="form.model.{{.JsonField}}" />
|
||||
{{- end}}
|
||||
</el-form-item>
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="form.close">{{ "{{" }} $t('common.dialogCancel') {{ "}}" }}</el-button>
|
||||
<el-button type="primary" :loading="form.submitting" @click="form.submit">
|
||||
{{ "{{" }} $t('common.dialogConfirm') {{ "}}" }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {add{{.ClassName}}, del{{.ClassName}}, get{{.ClassName}}, list{{.ClassName}}, update{{.ClassName}}} from '@/api/{{ .PackageName}}/{{ .MLTBName}}'
|
||||
{{ $package:=.PackageName }}
|
||||
{{range .Columns}}
|
||||
{{- if ne .FkTableName "" -}}
|
||||
import {list{{.FkTableNameClass}} } from '@/api/{{ $package }}/{{ .FkTableNamePackage}}'
|
||||
{{ end -}}
|
||||
{{- end -}}
|
||||
<script setup lang="ts">
|
||||
{{- if $hasRules}}
|
||||
import { computed } from 'vue'
|
||||
{{- end}}
|
||||
{{- if $hasFk}}
|
||||
import { ref, onMounted } from 'vue'
|
||||
{{- end}}
|
||||
{{- if $hasRules}}
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { FormRules } from 'element-plus'
|
||||
{{- end}}
|
||||
import PageContainer from '@/components/PageContainer/index.vue'
|
||||
import ProTable from '@/components/ProTable/index.vue'
|
||||
{{- if $hasDatetime}}
|
||||
import DateCell from '@/components/DateCell/index.vue'
|
||||
{{- end}}
|
||||
{{- if $hasDict}}
|
||||
{{- if $hasDictList}}
|
||||
import { useTable, useForm, useRemove, useDict, dictLabel } from '@/composables'
|
||||
{{- else}}
|
||||
import { useTable, useForm, useRemove, useDict } from '@/composables'
|
||||
{{- end}}
|
||||
{{- else}}
|
||||
import { useTable, useForm, useRemove } from '@/composables'
|
||||
{{- end}}
|
||||
import {
|
||||
add{{.ClassName}}, del{{.ClassName}}, get{{.ClassName}}, list{{.ClassName}}, update{{.ClassName}}
|
||||
} from '@/api/{{.PackageName}}/{{.MLTBName}}'
|
||||
import type { {{.ClassName}}, {{.ClassName}}Query } from '@/api/{{.PackageName}}/{{.MLTBName}}'
|
||||
{{- /*
|
||||
Two columns pointing at the same foreign table must not import it twice --
|
||||
"one FK-configured column" was never the same thing as "one distinct target
|
||||
table", and gen.go has no concept of a table's FK targets being unique.
|
||||
text/template has no set to check membership in, so the dedup is a nested
|
||||
range: a column only imports its target if no earlier, equally-used column
|
||||
already claimed the same FkTableNameClass. $fkUsed is repeated here (it also
|
||||
guards the const declarations above) because a column with FkTableName set
|
||||
but reaching none of them -- unqueried, unlisted, not an insert select --
|
||||
has nothing that would use the import either.
|
||||
*/ -}}
|
||||
{{- range $i, $col := .Columns}}
|
||||
{{- $fkUsed := and (ne $col.FkTableName "") (or (eq $col.IsQuery "1") (eq $col.IsList "1") (and (eq $col.IsInsert "1") (eq $col.HtmlType "select"))) -}}
|
||||
{{- if $fkUsed}}
|
||||
{{- $alreadyImported := false -}}
|
||||
{{- range $j, $prior := $.Columns}}
|
||||
{{- if lt $j $i}}
|
||||
{{- $priorUsed := and (ne $prior.FkTableName "") (or (eq $prior.IsQuery "1") (eq $prior.IsList "1") (and (eq $prior.IsInsert "1") (eq $prior.HtmlType "select"))) -}}
|
||||
{{- if and $priorUsed (eq $prior.FkTableNameClass $col.FkTableNameClass) }}{{$alreadyImported = true}}{{end -}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- if not $alreadyImported}}
|
||||
import { list{{$col.FkTableNameClass}} } from '@/api/{{$package}}/{{$col.FkTableNamePackage}}'
|
||||
import type { {{$col.FkTableNameClass}} } from '@/api/{{$package}}/{{$col.FkTableNamePackage}}'
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- /*
|
||||
Manage suffix, not just ClassName: this must match gen.go's
|
||||
Cmenu.MenuName = tab.ClassName + "Manage" byte for byte (PRD 010 R4), or
|
||||
keep-alive's include list -- built from menu_name -- never matches this
|
||||
component's name and the page never caches. The old template wrote
|
||||
name: '{ClassName}' with no suffix; the mismatch went unnoticed because
|
||||
stores/permission.ts's loadView() rewrites the rendered component's name to
|
||||
menu_name at runtime regardless of what defineOptions said (PRD 010 G7).
|
||||
That fallback stays in place after this change -- it is not this template's
|
||||
to remove -- but the value declared here should be right regardless of it.
|
||||
*/}}
|
||||
|
||||
export default {
|
||||
name: '{{.ClassName}}',
|
||||
components: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 选中数组
|
||||
ids: [],
|
||||
// 非单个禁用
|
||||
single: true,
|
||||
// 非多个禁用
|
||||
multiple: true,
|
||||
// 总条数
|
||||
total: 0,
|
||||
// 弹出层标题
|
||||
title: '',
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
isEdit: false,
|
||||
// 类型数据字典
|
||||
typeOptions: [],
|
||||
{{.BusinessName}}List: [],
|
||||
{{range .Columns}}
|
||||
{{- if ne .DictType "" -}}
|
||||
{{.JsonField}}Options: [],
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
// 关系表类型
|
||||
{{range .Columns}}
|
||||
{{- if ne .FkTableName "" -}}
|
||||
{{.JsonField}}Options :[],
|
||||
{{ end -}}
|
||||
{{- end }}
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
{{ range .Columns }}
|
||||
{{- if (.IsQuery) -}}
|
||||
{{.JsonField}}:undefined,
|
||||
{{ end -}}
|
||||
{{- end }}
|
||||
},
|
||||
// 表单参数
|
||||
form: {
|
||||
},
|
||||
// 表单校验
|
||||
rules: {
|
||||
{{- range .Columns -}}
|
||||
{{- $x := .IsQuery -}}
|
||||
{{- if (eq $x "1") -}}
|
||||
{{.JsonField}}: [ {required: true, message: '{{.ColumnComment}}不能为空', trigger: 'blur'} ],
|
||||
{{ end }}
|
||||
{{- end -}}
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
{{range .Columns}}
|
||||
{{- if ne .DictType "" -}}
|
||||
this.getDicts('{{.DictType}}').then(response => {
|
||||
this.{{.JsonField}}Options = response.data
|
||||
})
|
||||
{{ end -}}
|
||||
{{- if ne .FkTableName "" -}}
|
||||
this.get{{.FkTableNameClass}}Items()
|
||||
{{ end -}}
|
||||
{{- end -}}
|
||||
},
|
||||
methods: {
|
||||
/** 查询参数列表 */
|
||||
getList() {
|
||||
this.loading = true
|
||||
list{{.ClassName}}(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
|
||||
this.{{.BusinessName}}List = response.data.list
|
||||
this.total = response.data.count
|
||||
this.loading = false
|
||||
}
|
||||
)
|
||||
},
|
||||
// 取消按钮
|
||||
cancel() {
|
||||
this.open = false
|
||||
this.reset()
|
||||
},
|
||||
// 表单重置
|
||||
reset() {
|
||||
this.form = {
|
||||
{{ range .Columns}}
|
||||
{{- $x := .IsInsert -}}
|
||||
{{- if (eq $x "1") -}}
|
||||
{{- if eq .GoField "CreatedAt" -}}
|
||||
{{- else if eq .GoField "UpdatedAt" -}}
|
||||
{{- else if eq .GoField "DeletedAt" -}}
|
||||
{{- else if eq .GoField "UpdateBy" -}}
|
||||
{{- else if eq .GoField "CreateBy" -}}
|
||||
{{- else }}
|
||||
{{.JsonField}}: undefined,
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
}
|
||||
this.resetForm('form')
|
||||
},
|
||||
getImgList: function() {
|
||||
this.form[this.fileIndex] = this.$refs['fileChoose'].resultList[0].fullUrl
|
||||
},
|
||||
fileClose: function() {
|
||||
this.fileOpen = false
|
||||
},
|
||||
{{range .Columns}}
|
||||
{{- if ne .DictType "" -}}
|
||||
{{.JsonField}}Format(row) {
|
||||
return this.selectDictLabel(this.{{.JsonField}}Options, row.{{.JsonField}})
|
||||
},
|
||||
{{ end -}}
|
||||
{{- if ne .FkTableName "" -}}
|
||||
{{.JsonField}}Format(row) {
|
||||
return this.selectItemsLabel(this.{{.JsonField}}Options, row.{{.JsonField}})
|
||||
},
|
||||
{{ end -}}
|
||||
{{- end -}}
|
||||
// 关系
|
||||
{{range .Columns}}
|
||||
{{- if ne .FkTableName "" -}}
|
||||
get{{.FkTableNameClass}}Items() {
|
||||
this.getItems(list{{.FkTableNameClass}}, undefined).then(res => {
|
||||
this.{{.JsonField}}Options = this.setItems(res, '{{.FkLabelId}}', '{{.FkLabelName}}')
|
||||
})
|
||||
},
|
||||
{{ end -}}
|
||||
{{- end -}}
|
||||
// 文件
|
||||
{{range .Columns}}
|
||||
{{- if eq .HtmlType "file" -}}
|
||||
fileShow{{.GoField}}: function() {
|
||||
this.fileOpen = true
|
||||
this.fileIndex = '{{.JsonField}}'
|
||||
},
|
||||
{{ end -}}
|
||||
{{- end -}}
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery() {
|
||||
this.queryParams.pageIndex = 1
|
||||
this.getList()
|
||||
},
|
||||
/** 重置按钮操作 */
|
||||
resetQuery() {
|
||||
this.dateRange = []
|
||||
this.resetForm('queryForm')
|
||||
this.handleQuery()
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
handleAdd() {
|
||||
this.reset()
|
||||
this.open = true
|
||||
this.title = '添加{{.TableComment}}'
|
||||
this.isEdit = false
|
||||
},
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.{{.PkJsonField}})
|
||||
this.single = selection.length !== 1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate(row) {
|
||||
this.reset()
|
||||
const {{.PkJsonField}} =
|
||||
row.{{.PkJsonField}} || this.ids
|
||||
get{{.ClassName}}({{.PkJsonField}}).then(response => {
|
||||
this.form = response.data
|
||||
this.open = true
|
||||
this.title = '修改{{.TableComment}}'
|
||||
this.isEdit = true
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm: function () {
|
||||
this.$refs['form'].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.{{.PkJsonField}} !== undefined) {
|
||||
update{{.ClassName}}(this.form).then(response => {
|
||||
if (response.code === 200) {
|
||||
this.msgSuccess(response.msg)
|
||||
this.open = false
|
||||
this.getList()
|
||||
} else {
|
||||
this.msgError(response.msg)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
add{{.ClassName}}(this.form).then(response => {
|
||||
if (response.code === 200) {
|
||||
this.msgSuccess(response.msg)
|
||||
this.open = false
|
||||
this.getList()
|
||||
} else {
|
||||
this.msgError(response.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
var Ids = (row.{{.PkJsonField}} && [row.{{.PkJsonField}}]) || this.ids
|
||||
defineOptions({ name: '{{.ClassName}}Manage' })
|
||||
{{- range .Columns}}
|
||||
{{- $dictUsed := and (ne .DictType "") (or (and (eq .IsQuery "1") (eq .FkTableName "")) (and (eq .IsList "1") (eq .FkTableName "")) (and (eq .IsInsert "1") (eq .HtmlType "select") (eq .FkTableName "")) (and (eq .IsInsert "1") (eq .HtmlType "radio"))) -}}
|
||||
{{- $fkUsed := and (ne .FkTableName "") (or (eq .IsQuery "1") (eq .IsList "1") (and (eq .IsInsert "1") (eq .HtmlType "select"))) -}}
|
||||
{{- if $dictUsed}}
|
||||
|
||||
this.$confirm('是否确认删除编号为"' + Ids + '"的数据项?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(function () {
|
||||
return del{{.ClassName}}( { 'ids': Ids })
|
||||
}).then((response) => {
|
||||
if (response.code === 200) {
|
||||
this.msgSuccess(response.msg)
|
||||
this.open = false
|
||||
this.getList()
|
||||
} else {
|
||||
this.msgError(response.msg)
|
||||
}
|
||||
}).catch(function () {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
const { {{.DictType}}: {{.JsonField}}DictOptions } = useDict('{{.DictType}}')
|
||||
{{- end}}
|
||||
{{- if $fkUsed}}
|
||||
|
||||
const {{.JsonField}}FkOptions = ref<{{.FkTableNameClass}}[]>([])
|
||||
onMounted(async() => {
|
||||
const res = await list{{.FkTableNameClass}}({ pageIndex: 1, pageSize: 100 })
|
||||
{{.JsonField}}FkOptions.value = res.data?.list ?? []
|
||||
})
|
||||
{{- if eq .IsList "1"}}
|
||||
const {{.JsonField}}Label = (value: unknown) =>
|
||||
{{.JsonField}}FkOptions.value.find(item => item.{{.FkLabelId}} === value)?.{{.FkLabelName}} ?? value
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- /*
|
||||
Every object literal below is built on one line, joined with ", " through a
|
||||
$first flag rather than one field per line with a trailing comma after each:
|
||||
comma-dangle is "never" (no comma before the closing brace) and comma-style
|
||||
is "last" (a comma may not open a line), and text/template has no arithmetic
|
||||
to compute "is this the last matching column" up front -- knowing that would
|
||||
be what a one-field-per-line, trailing-comma-free rendering needs instead.
|
||||
*/}}
|
||||
|
||||
const table = useTable<{{.ClassName}}, {{.ClassName}}Query>({
|
||||
api: list{{.ClassName}},
|
||||
idKey: '{{.PkJsonField}}'
|
||||
{{- if $hasQuery}},
|
||||
defaultQuery: () => ({{"{"}} {{$qFirst := true}}{{range .Columns}}{{if eq .IsQuery "1"}}{{if $qFirst}}{{$qFirst = false}}{{else}}, {{end}}{{.JsonField}}: undefined{{end}}{{end}} {{"}"}})
|
||||
{{- end}}
|
||||
})
|
||||
{{- if $hasRules}}
|
||||
|
||||
const { t } = useI18n()
|
||||
{{- /*
|
||||
Built from the same field-label key rather than a dedicated
|
||||
gen.{pkg}.{biz}.rules.{field} key: R3 derives one key per field from
|
||||
PackageName+BusinessName+JsonField, and a second, validation-only key per
|
||||
required field would double the language pack's surface for a message that
|
||||
reads fine as the field name alone in the space Element Plus renders it --
|
||||
directly under the labelled field it failed to validate.
|
||||
*/}}
|
||||
|
||||
const rules = computed<FormRules>(() => ({ {{$rFirst := true}}
|
||||
{{- range .Columns}}
|
||||
{{- if and (eq .IsInsert "1") (eq .IsRequired "1") (not .Pk) (ne .GoField "CreatedAt") (ne .GoField "UpdatedAt") (ne .GoField "DeletedAt") (ne .GoField "UpdateBy") (ne .GoField "CreateBy")}}
|
||||
{{- $key := printf "gen.%s.%s.%s" $package $business .JsonField}}
|
||||
{{- if $rFirst}}{{$rFirst = false}}{{else}},
|
||||
{{end}}{{.JsonField}}: [{ required: true, message: t('{{$key}}'), trigger: '{{if or (eq .HtmlType "select") (eq .HtmlType "radio") (eq .HtmlType "datetime") (eq .HtmlType "checkbox")}}change{{else}}blur{{end}}' }]
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}))
|
||||
{{- end}}
|
||||
|
||||
const form = useForm<{{.ClassName}}, {{$pkType}}>({
|
||||
defaultModel: () => ({{"{"}} {{.PkJsonField}}: undefined{{range .Columns}}{{if and (eq .IsInsert "1") (not .Pk) (ne .GoField "CreatedAt") (ne .GoField "UpdatedAt") (ne .GoField "DeletedAt") (ne .GoField "UpdateBy") (ne .GoField "CreateBy")}}, {{.JsonField}}: {{if eq .DefaultValue ""}}undefined{{else if eq .GoType "int"}}{{.DefaultValue}}{{else}}'{{js .DefaultValue}}'{{end}}{{end}}{{end}} {{"}"}}),
|
||||
idKey: '{{.PkJsonField}}',
|
||||
{{- if $hasRules}}
|
||||
rules,
|
||||
{{- end}}
|
||||
api: { get: get{{.ClassName}}, add: add{{.ClassName}}, update: update{{.ClassName}} },
|
||||
onSuccess: () => table.getList()
|
||||
})
|
||||
|
||||
const { remove } = useRemove({
|
||||
api: del{{.ClassName}},
|
||||
onSuccess: () => table.getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user