chore: 初始开源发布

iotlcm-app:小程序端 + 用户端服务。

- app/:uni-app 小程序前端(基于 unibest,MIT)
  - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID
  - 内网后端地址 → admin.iotlcm.example.com
  - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明)
- server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0)
  - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件)
  - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化
  - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位)
  - 依赖号码尾部的断言同步更新
  - 补 server/LICENSE(Apache-2.0)
- 移除内部资产:CI 触发器
- 补根 README,LF 行尾 + .gitattributes

已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
This commit is contained in:
MiFaZhan
2026-10-26 14:01:25 +08:00
commit 2ed5dfa6a1
1013 changed files with 138466 additions and 0 deletions
@@ -0,0 +1,86 @@
// package genconsts
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genconsts
// 生成类型值定义
const (
GenCodesTypeCurd = 0
GenCodesTypeTree = 1
)
// 生成类型标签映射
var GenCodesTypeNameMap = map[int]string{
GenCodesTypeCurd: "增删改查列表",
GenCodesTypeTree: "关系树列表",
}
// 生成类型配置映射
var GenCodesTypeConfMap = map[int]string{
GenCodesTypeCurd: "crud",
GenCodesTypeTree: "tree",
}
// 生成代码状态
const (
GenCodesStatusOk = "0" // 生成成功
GenCodesStatusWait = "1" // 未生成
GenCodesStatusFail = "2" // 生成失败
)
var GenCodesStatusNameMap = map[string]string{
GenCodesStatusOk: "生成成功",
GenCodesStatusWait: "未生成",
GenCodesStatusFail: "生成失败",
}
// 生成代码关联表方式
const (
GenCodesJoinLeft = 1 // 左关联
GenCodesJoinRight = 2 // 右关联
GenCodesJoinInner = 3 // 内关联
)
var GenCodesJoinNameMap = map[int]string{
GenCodesJoinLeft: "左关联",
GenCodesJoinRight: "右关联",
GenCodesJoinInner: "内关联",
}
var GenCodesJoinLinkMap = map[int]string{
GenCodesJoinLeft: "LeftJoin",
GenCodesJoinRight: "RightJoin",
GenCodesJoinInner: "InnerJoin",
}
// 生成代码的生成方式
const (
GenCodesBuildMethCreate = 1 // 创建
GenCodesBuildMethCover = 2 // 覆盖
GenCodesBuildMethSkip = 3 // 跳过
GenCodesBuildIgnore = 4 // 不生成
)
var GenCodesBuildMethNameMap = map[int]string{
GenCodesBuildMethCreate: "创建文件",
GenCodesBuildMethCover: "强制覆盖",
GenCodesBuildMethSkip: "已存在跳过",
GenCodesBuildIgnore: "不生成",
}
const (
GenCodesIndexPK = "PRI" // 主键索引
GenCodesIndexUNI = "UNI" // 唯一索引
)
const (
GenCodesTreeStyleTypeNormal = 1 // 普通树表格
GenCodesTreeStyleTypeOption = 2 // 选项式树表
)
var GenCodesTreeStyleTypeNameMap = map[int]string{
GenCodesTreeStyleTypeNormal: "普通树表格",
GenCodesTreeStyleTypeOption: "选项式树表",
}
@@ -0,0 +1,87 @@
// package genconsts
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genconsts
import "github.com/gogf/gf/v2/util/gconv"
// 配置数据类型
const (
ConfigTypeString = "string"
ConfigTypeInt = "int"
ConfigTypeInt8 = "int8"
ConfigTypeInt16 = "int16"
ConfigTypeInt32 = "int32"
ConfigTypeInt64 = "int64"
ConfigTypeUint = "uint"
ConfigTypeUint8 = "uint8"
ConfigTypeUint16 = "uint16"
ConfigTypeUint32 = "uint32"
ConfigTypeUint64 = "uint64"
ConfigTypeFloat32 = "float32"
ConfigTypeFloat64 = "float64"
ConfigTypeBool = "bool"
ConfigTypeDate = "date"
ConfigTypeDateTime = "datetime"
ConfigTypeSliceString = "[]string"
ConfigTypeSliceInt = "[]int"
ConfigTypeSliceInt64 = "[]int64"
)
var ConfigTypes = []string{ConfigTypeString,
ConfigTypeInt, ConfigTypeInt8, ConfigTypeInt16, ConfigTypeInt32, ConfigTypeInt64,
ConfigTypeUint, ConfigTypeUint8, ConfigTypeUint16, ConfigTypeUint32, ConfigTypeUint64,
ConfigTypeFloat32, ConfigTypeFloat64,
ConfigTypeBool,
ConfigTypeDate, ConfigTypeDateTime,
ConfigTypeSliceString, ConfigTypeSliceInt, ConfigTypeSliceInt64,
}
// ConvType 类型转换
func ConvType(val interface{}, t string) interface{} {
switch t {
case ConfigTypeString:
val = gconv.String(val)
case ConfigTypeInt:
val = gconv.Int(val)
case ConfigTypeInt8:
val = gconv.Int8(val)
case ConfigTypeInt16:
val = gconv.Int16(val)
case ConfigTypeInt32:
val = gconv.Int32(val)
case ConfigTypeInt64:
val = gconv.Int64(val)
case ConfigTypeUint:
val = gconv.Uint(val)
case ConfigTypeUint8:
val = gconv.Uint8(val)
case ConfigTypeUint16:
val = gconv.Uint16(val)
case ConfigTypeUint32:
val = gconv.Uint32(val)
case ConfigTypeUint64:
val = gconv.Uint64(val)
case ConfigTypeFloat32:
val = gconv.Float32(val)
case ConfigTypeFloat64:
val = gconv.Float64(val)
case ConfigTypeBool:
val = gconv.Bool(val)
case ConfigTypeDate:
val = gconv.Time(val, "Y-m-d")
case ConfigTypeDateTime:
val = gconv.Time(val, "Y-m-d H:i:s")
case ConfigTypeSliceInt:
val = gconv.SliceInt(val)
case ConfigTypeSliceInt64:
val = gconv.SliceInt64(val)
case ConfigTypeSliceString:
val = gconv.SliceStr(val)
default:
val = gconv.String(val)
}
return val
}
@@ -0,0 +1,65 @@
// package gendao
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package gendao
import (
"context"
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/util/gconv"
)
var (
daoConfigList = make([]*genmodel.GenDaoConfig, 0)
cliConfigPath = `hack/config.yaml`
cliConfig *gcfg.Config
defaultGenServiceConfig = genmodel.GenServiceConfig{
SrcFolder: "internal/logic",
DstFolder: "internal/service",
DstFileNameCase: "Snake",
StPattern: `s([A-Z]\w+)`,
// gf gen service -l currently has a Windows path handling issue that
// can remove all generated service files. Keep generation portable.
Clear: false,
}
serviceConfig = genmodel.GenServiceConfig{}
)
func GetDaoConfig(ctx context.Context, group string) (output *genmodel.GenDaoConfig, err error) {
for _, v := range daoConfigList {
if v == nil {
continue
}
if v.Group == group {
output = v
return
}
}
return
}
func Init(ctx context.Context) (err error) {
adapter, err := gcfg.NewAdapterFile(cliConfigPath)
if err != nil {
g.Log().Fatalf(ctx, "get cli configuration file:%v, err:%+v", cliConfigPath, err)
return
}
cliConfig = gcfg.NewWithAdapter(adapter)
cliConfig.MustGet(ctx, "gfcli.gen.dao").Scan(&daoConfigList)
cliConfig.MustGet(ctx, "gfcli.gen.service").Scan(&serviceConfig)
return
}
func GetServiceConfig() *genmodel.GenServiceConfig {
conf := &genmodel.GenServiceConfig{}
_ = gconv.Scan(defaultGenServiceConfig, &conf)
_ = gconv.Scan(serviceConfig, &conf)
return conf
}
@@ -0,0 +1,146 @@
// package genmodel
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genmodel
import "github.com/gogf/gf/v2/encoding/gjson"
type SelectItemModel struct {
Value interface{} `json:"value"` // 选择项值
Label string `json:"label"` // 选择项标签
Name string `json:"name"` // 选择项名称
Disabled bool `json:"disabled"` // 是否禁用
}
// 生成类型选择项
type SelectGenTypeModel struct {
SelectItemModel
Templates []*SelectGenTemplateModel `json:"templates"` // 选择项模板
}
// 生成模板选择项
type SelectGenTemplateModel struct {
SelectItemModel
IsAddon bool `json:"isAddon"` // 是否为插件
}
type SelectsModel struct {
GenType []*SelectGenTypeModel `json:"genType"` // 生成类型 0:增删改查列表 1:关系树列表
Db []*SelectItemModel `json:"db"` // 数据库选择项
Status []*SelectItemModel `json:"status"` // 状态选择项 0:生成成功 1:未生产 2:生成失败
LinkMode []*SelectItemModel `json:"linkMode"` // 链接模式选择项 1左关联 2右关联 3内关联
BuildMethod []*SelectItemModel `json:"buildMethod"` // 构建标志选择项 1:创建文件 2强制覆盖 3已存在跳过 4不生成
FormMode []*SelectItemModel `json:"formMode"` // 表单项类型选择项
FormRole []*SelectItemModel `json:"formRole"` // 表单项校验规则选择项
DictMode []*SelectItemModel `json:"dictMode"` // 字典模式选择项
WhereMode []*SelectItemModel `json:"whereMode"` // 查询条件选择项
Addons []*SelectItemModel `json:"addons"` // 插件选择项
TableAlign []*SelectItemModel `json:"tableAlign"` // 表格对齐方式选择项
TreeStyleType []*SelectItemModel `json:"treeStyleType"` // 树形结构选择项
}
// GenCodesColumn 生成表字段属性
type GenCodesColumn struct {
// 表属性
Id int64 `json:"id" dc:"序号"`
Name string `json:"name" dc:"字段列名"`
Dc string `json:"dc" dc:"字段描述"`
DataType string `json:"dataType" dc:"字段类型"`
SqlType string `json:"sqlType" dc:"物理类型"`
Length int64 `json:"length" dc:"字段长度"`
IsAllowNull string `json:"isAllowNull" dc:"是否允许为空"`
DefaultValue interface{} `json:"defaultValue" dc:"默认值"`
Index string `json:"index" dc:"索引"`
Extra string `json:"extra" dc:"额外选项"`
// 自定义生成属性
// Alias string `json:"alias" dc:"字段别名"`
GoName string `json:"goName" dc:"Go属性"`
GoType string `json:"goType" dc:"Go类型"`
TsName string `json:"tsName" dc:"Ts属性"`
TsType string `json:"tsType" dc:"Ts类型"`
IsList bool `json:"isList" dc:"列表"`
IsExport bool `json:"isExport" dc:"导出"`
IsSort bool `json:"isSort" dc:"排序"`
IsQuery bool `json:"isQuery" dc:"查询"`
QueryWhere string `json:"queryWhere" dc:"查询条件"`
IsEdit bool `json:"isEdit" dc:"编辑"`
Required bool `json:"required" dc:"必填"`
Unique bool `json:"unique" dc:"唯一性"`
FormMode string `json:"formMode" dc:"表单组件"`
FormRole string `json:"formRole" dc:"表单验证"`
FormGridSpan int `json:"formGridSpan" dc:"栅格占位数量"`
DictType string `json:"dictType" dc:"字典类型ID"`
Align string `json:"align" dc:"排列方式"`
Width int64 `json:"width" dc:"列宽"`
}
type GenCodesTableSelectParam struct {
DbGroup string `json:"dbGroup"`
}
type GenCodesTableSelectModel struct {
Value string `json:"value"`
Label string `json:"label"`
Name string `json:"name"`
DaoName string `json:"daoName" dc:"orm模型名称"`
DefVarName string `json:"defVarName" dc:"默认实体名称"`
DefAlias string `json:"defAlias" dc:"默认关联表别名"`
DefTableComment string `json:"defTableComment" dc:"默认菜单名称"`
}
type GenCodesColumnSelectParam struct {
DbGroup string `json:"dbGroup"`
TableName string `json:"tableName"`
}
type GenCodesColumnSelectModel struct {
Value string `json:"value"`
Label string `json:"label"`
Name string `json:"name"`
}
type GenCodesColumnListParam struct {
DbGroup string `json:"dbGroup" dc:"数据库配置名称"`
TableName string `json:"tableName" dc:"表名称"`
IsLink int64 `json:"isLink" dc:"是否是关联表"`
Alias string `json:"alias" dc:"关联表别名"`
}
type GenCodesColumnListModel struct {
GenCodesColumn
}
type GenCodesPreviewParam struct {
TableId int64 `json:"tableId" orm:"table_id" description:"生成ID"`
GenType uint `json:"genType" orm:"gen_type" description:"生成类型"`
GenTemplate int `json:"genTemplate" orm:"gen_template" description:"生成模板"`
VarName string `json:"varName" orm:"var_name" description:"实体命名"`
Options *gjson.Json `json:"options" orm:"options" description:"配置选项"`
DbName string `json:"dbName" orm:"db_name" description:"数据库名称"`
TableName string `json:"tableName" orm:"table_name" description:"主表名称"`
TableComment string `json:"tableComment" orm:"table_comment" description:"主表注释"`
DaoName string `json:"daoName" orm:"dao_name" description:"主表dao模型"`
MasterColumns *gjson.Json `json:"masterColumns" orm:"master_columns" description:"主表字段"`
AddonName string `json:"addonName" orm:"addon_name" description:"插件名称"`
Status int `json:"status" orm:"status" description:"生成状态"`
}
// GenFile 生成文件配置
type GenFile struct {
Content string `json:"content" dc:"页面内容"`
Path string `json:"path" dc:"生成路径"`
Meth int `json:"meth" dc:"生成方式"`
Required bool `json:"required" dc:"是否是必要构建文件"`
}
type GenCodesPreviewModel struct {
Config *GenCodesConfig `json:"config"`
Views map[string]*GenFile `json:"views" dc:"页面"`
}
type GenCodesBuildParam = GenCodesPreviewParam
type GenCodesBuildModel struct {
}
@@ -0,0 +1,126 @@
// package genmodel
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genmodel
import (
genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts"
"github.com/gogf/gf/v2/util/gconv"
)
// 模板配置 config/config.yaml xgen.application.crud.templates
type GenCodesConfigCrudTemplate struct {
Group string `name:"group" json:"group"`
IsAddon bool `name:"isAddon" json:"isAddon"`
MasterPackage string `name:"masterPackage" json:"masterPackage"`
TemplatePath string `name:"templatePath" json:"templatePath"`
ApiPath string `name:"apiPath" json:"apiPath"`
InputPath string `name:"inputPath" json:"inputPath"`
ControllerPath string `name:"controllerPath" json:"controllerPath"`
LogicPath string `name:"logicPath" json:"logicPath"`
RouterPath string `name:"routerPath" json:"routerPath"`
SqlPath string `name:"sqlPath" json:"sqlPath"`
WebApiPath string `name:"webApiPath" json:"webApiPath"`
WebViewsPath string `name:"webViewsPath" json:"webViewsPath"`
}
type GenCodesConfigTreeTemplate struct {
Group string `json:"group"`
TemplatePath string `json:"templatePath"`
}
type GenCodesConfigBuildAddon struct {
SrcPath string `json:"srcPath"`
WebApiPath string `json:"webApiPath"`
WebViewsPath string `json:"webViewsPath"`
}
// 工具生成配置 hack/config.yaml gfcli.gen.dao
type GenDaoConfig struct {
Group string `name:"group" json:"group"`
Link string `name:"link" json:"link"`
DbName string `name:"dbName" json:"dbName"`
TableName string `name:"tableName" json:"tableName"`
JsonCase string `name:"jsonCase" json:"jsonCase"`
StdTime bool `name:"stdTime" json:"stdTime"`
GJsonSupport bool `name:"gJsonSupport" json:"gJsonSupport"`
}
// 工具生成配置 hack/config.yaml gfcli.gen.service
type GenServiceConfig struct {
SrcFolder string `name:"srcFolder"`
DstFolder string `name:"dstFolder"`
DstFileNameCase string `name:"dstFileNameCase"`
WatchFile string `name:"watchFile" `
StPattern string `name:"stPattern" `
Packages []string `name:"packages" `
ImportPrefix string `name:"importPrefix" `
Clear bool `name:"clear" `
}
// 模板配置 config/config.yaml xgen
type GenCodesConfig struct {
AllowedIPs []string `json:"allowedIPs"`
Application struct {
Crud struct {
Templates []*GenCodesConfigCrudTemplate `json:"templates"`
} `json:"crud"`
Tree struct {
Templates []*GenCodesConfigTreeTemplate `json:"templates"`
} `json:"tree"`
} `json:"application"`
Delimiters []string `json:"delimiters"`
DevPath string `json:"devPath"`
DisableTables []string `json:"disableTables"`
SelectDbs []string `json:"selectDbs"`
Addon *GenCodesConfigBuildAddon `json:"addon"`
}
// ConvType 类型转换
func ConvType(val interface{}, t string) interface{} {
switch t {
case genconsts.ConfigTypeString:
val = gconv.String(val)
case genconsts.ConfigTypeInt:
val = gconv.Int(val)
case genconsts.ConfigTypeInt8:
val = gconv.Int8(val)
case genconsts.ConfigTypeInt16:
val = gconv.Int16(val)
case genconsts.ConfigTypeInt32:
val = gconv.Int32(val)
case genconsts.ConfigTypeInt64:
val = gconv.Int64(val)
case genconsts.ConfigTypeUint:
val = gconv.Uint(val)
case genconsts.ConfigTypeUint8:
val = gconv.Uint8(val)
case genconsts.ConfigTypeUint16:
val = gconv.Uint16(val)
case genconsts.ConfigTypeUint32:
val = gconv.Uint32(val)
case genconsts.ConfigTypeUint64:
val = gconv.Uint64(val)
case genconsts.ConfigTypeFloat32:
val = gconv.Float32(val)
case genconsts.ConfigTypeFloat64:
val = gconv.Float64(val)
case genconsts.ConfigTypeBool:
val = gconv.Bool(val)
case genconsts.ConfigTypeDate:
val = gconv.Time(val, "Y-m-d")
case genconsts.ConfigTypeDateTime:
val = gconv.Time(val, "Y-m-d H:i:s")
case genconsts.ConfigTypeSliceInt:
val = gconv.SliceInt(val)
case genconsts.ConfigTypeSliceInt64:
val = gconv.SliceInt64(val)
case genconsts.ConfigTypeSliceString:
val = gconv.SliceStr(val)
default:
val = gconv.String(val)
}
return val
}
@@ -0,0 +1,145 @@
// package genmodel
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genmodel
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gview"
)
// ImportModel 导包 - model.ts
type ImportModel struct {
NaiveUI []string
UtilsIs []string
UtilsUrl []string
UtilsDate []string
UtilsValidate []string
UtilsIndex []string
}
// CurdStep 生成选项
type CurdStep struct {
HasMaxSort bool // 最大排序
HasAdd bool // 表单添加
HasBatchDel bool // 批量删除
HasExport bool // 表格导出
HasNotFilterAuth bool // 不过滤认证权限
HasEdit bool // 表单编辑
HasDel bool // 删除
HasView bool // 查看详情
HasStatus bool // 修改状态
HasSwitch bool // 数值开关
HasCheck bool // 勾选列
HasMenu bool // 菜单权限
IsTreeTable bool // 树型列表
IsOptionTreeTable bool // 选项式树型列表
HasRules bool // 表单验证规则
HasRulesValidator bool // 表单验证器
HasSearchForm bool // 列表搜索
HasDict bool // 字典
HasFuncDict bool // 注册方法字典
HasQueryMemberSummary bool // 查询用户摘要
HasHookMemberSummary bool // hook用户摘要
ImportModel ImportModel // 公用导包 - model.ts
ActionColumnWidth int64 // 列表操作栏宽度
IsAddon bool // 是否是插件
IsEditModal bool // 新增修改模态框模式
}
type FuncDict struct {
ValueColumn string // 选项值
LabelColumn string // 选项名称
Value *GenCodesColumnListModel
Label *GenCodesColumnListModel
}
// PresetStep 预设生成流程参数
type PresetStep struct {
FormGridCols int `json:"formGridCols" dc:"表单显示的栅格数量"`
}
// OptionsTree 树形结构选项
type OptionsTree struct {
TitleColumn string `json:"titleColumn"`
PidColumn string `json:"pidColumn"`
LevelColumn string `json:"levelColumn"`
TreeColumn string `json:"treeColumn"`
StyleType int `json:"styleType"`
TitleField *GenCodesColumnListModel
PidField *GenCodesColumnListModel
LevelField *GenCodesColumnListModel
TreeField *GenCodesColumnListModel
HasTreePath bool
}
// CurdOptionsMenu 菜单选项
type CurdOptionsMenu struct {
Icon string `json:"icon"`
Pid int `json:"pid"`
Sort int `json:"sort"`
}
// CurdOptionsJoin 关联表选项
type CurdOptionsJoin struct {
Uuid string `json:"uuid"`
LinkTable string `json:"linkTable"`
Alias string `json:"alias"`
LinkMode int `json:"linkMode"`
Field string `json:"field"`
MasterField string `json:"masterField"`
DaoName string `json:"daoName"`
Columns []*GenCodesColumnListModel `json:"columns"`
}
type OptionsSchemasField struct {
Field string
Type string
}
type CurdOptionsDict struct {
Has bool
Types []string
Schemas []*OptionsSchemasField
}
// CurdOptions 生成选项
type CurdOptions struct {
AutoOps []string `json:"autoOps"`
ColumnOps []string `json:"columnOps"`
HeadOps []string `json:"headOps"`
Join []*CurdOptionsJoin `json:"join"`
Menu *CurdOptionsMenu `json:"menu"`
Tree *OptionsTree `json:"tree"`
TemplateGroup string `json:"templateGroup"`
ApiPrefix string `json:"apiPrefix"`
ImportWebApi string `json:"importWebApi"`
FuncDict *FuncDict `json:"funcDict"`
PresetStep *PresetStep `json:"presetStep"`
Step *CurdStep // 转换后的流程控制条件
DictOps CurdOptionsDict // 字典选项
DictMap g.Map // 字典选项 -> 字段映射关系
}
type CurdPreviewParam struct {
In *GenCodesPreviewParam // 提交参数
DaoConfig *GenDaoConfig // 生成dao配置
Config *GenCodesConfig // 生成配置
View *gview.View // 视图模板
Content *GenCodesPreviewModel // 页面代码
MasterFields []*GenCodesColumnListModel // 主表字段属性
Pk *GenCodesColumnListModel // 主键属性
Options *CurdOptions // 生成选项
}
type CurdBuildEvent map[string]func(ctx context.Context) (err error)
type CurdBuildParam struct {
PreviewIn *CurdPreviewParam // 预览参数
BeforeEvent CurdBuildEvent // 前置事件
AfterEvent CurdBuildEvent // 后置事件
}
@@ -0,0 +1,253 @@
// package genview
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genview
import (
"context"
gendao "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_dao"
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
"strings"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
func getJsonTagFromCase(str, caseStr string) string {
switch gstr.ToLower(caseStr) {
case gstr.ToLower("Camel"):
return gstr.CaseCamel(str)
case gstr.ToLower("CamelLower"):
return gstr.CaseCamelLower(str)
case gstr.ToLower("Kebab"):
return gstr.CaseKebab(str)
case gstr.ToLower("KebabScreaming"):
return gstr.CaseKebabScreaming(str)
case gstr.ToLower("Snake"):
return gstr.CaseSnake(str)
case gstr.ToLower("SnakeFirstUpper"):
return gstr.CaseSnakeFirstUpper(str)
case gstr.ToLower("SnakeScreaming"):
return gstr.CaseSnakeScreaming(str)
}
return str
}
func CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue interface{}) (string, error) {
var (
typeName string
typePattern string
)
match, _ := gregex.MatchString(`(.+?)\((.+)\)`, fieldType)
if len(match) == 3 {
typeName = gstr.Trim(match[1])
typePattern = gstr.Trim(match[2])
} else {
typeName = gstr.Split(fieldType, " ")[0]
}
typeName = strings.ToLower(typeName)
switch typeName {
case
"binary",
"varbinary",
"blob",
"tinyblob",
"mediumblob",
"longblob":
return string(gdb.LocalTypeBytes), nil
case
"int",
"tinyint",
"small_int",
"smallint",
"medium_int",
"mediumint",
"serial":
if gstr.ContainsI(fieldType, "unsigned") {
return string(gdb.LocalTypeUint), nil
}
return string(gdb.LocalTypeInt), nil
case
"big_int",
"bigint",
"bigserial":
if gstr.ContainsI(fieldType, "unsigned") {
return string(gdb.LocalTypeUint64), nil
}
return string(gdb.LocalTypeInt64), nil
case
"real":
return string(gdb.LocalTypeFloat32), nil
case
"float",
"double",
"decimal",
"money",
"numeric",
"smallmoney":
return string(gdb.LocalTypeFloat64), nil
case
"bit":
// It is suggested using bit(1) as boolean.
if typePattern == "1" {
return string(gdb.LocalTypeBool), nil
}
s := gconv.String(fieldValue)
// mssql is true|false string.
if strings.EqualFold(s, "true") || strings.EqualFold(s, "false") {
return string(gdb.LocalTypeBool), nil
}
if gstr.ContainsI(fieldType, "unsigned") {
return string(gdb.LocalTypeUint64Bytes), nil
}
return string(gdb.LocalTypeInt64Bytes), nil
case
"bool":
return string(gdb.LocalTypeBool), nil
case
"date":
return string(gdb.LocalTypeDate), nil
case
"datetime",
"timestamp",
"timestamptz":
return string(gdb.LocalTypeDatetime), nil
case
"json":
return string(gdb.LocalTypeJson), nil
case
"jsonb":
return string(gdb.LocalTypeJsonb), nil
default:
// Auto-detect field type, using key match.
switch {
case strings.Contains(typeName, "text") || strings.Contains(typeName, "char") || strings.Contains(typeName, "character"):
return string(gdb.LocalTypeString), nil
case strings.Contains(typeName, "float") || strings.Contains(typeName, "double") || strings.Contains(typeName, "numeric"):
return string(gdb.LocalTypeFloat64), nil
case strings.Contains(typeName, "bool"):
return string(gdb.LocalTypeBool), nil
case strings.Contains(typeName, "binary") || strings.Contains(typeName, "blob"):
return string(gdb.LocalTypeBytes), nil
case strings.Contains(typeName, "int"):
if gstr.ContainsI(fieldType, "unsigned") {
return string(gdb.LocalTypeUint), nil
}
return string(gdb.LocalTypeInt), nil
case strings.Contains(typeName, "time"):
return string(gdb.LocalTypeDatetime), nil
case strings.Contains(typeName, "date"):
return string(gdb.LocalTypeDatetime), nil
default:
return string(gdb.LocalTypeString), nil
}
}
}
// GenGotype 生成字段的go类型
func GenGotype(ctx context.Context, field *genmodel.GenCodesColumnListModel, in *genmodel.GenDaoConfig) (goName, typeName, tsName string, tsType string) {
var err error
tsName = getJsonTagFromCase(field.Name, in.JsonCase)
goName = gstr.CaseCamel(field.Name)
typeName, err = CheckLocalTypeForField(ctx, field.DataType, nil)
if err != nil {
panic(err)
}
switch gdb.LocalType(typeName) {
case gdb.LocalTypeDate, gdb.LocalTypeDatetime:
if in.StdTime {
typeName = "time.Time"
} else {
typeName = "*gtime.Time"
}
case gdb.LocalTypeInt64Bytes:
typeName = "int64"
case gdb.LocalTypeUint64Bytes:
typeName = "uint64"
// Special type handle.
case gdb.LocalTypeJson, gdb.LocalTypeJsonb:
if in.GJsonSupport {
typeName = "*gjson.Json"
} else {
typeName = "string"
}
}
tsType = ShiftMap[typeName]
return
}
func CustomLinkAttributes(ctx context.Context, alias string, field *genmodel.GenCodesColumnListModel, in *genmodel.GenDaoConfig) {
field.GoName, field.GoType, field.TsName, field.TsType = GenGotype(ctx, field, in)
field.GoName = gstr.UcFirst(alias + field.GoName)
field.TsName = gstr.LcFirst(field.GoName)
setDefaultQueryWhere(field)
setDefaultValue(field)
}
func CustomAttributes(ctx context.Context, field *genmodel.GenCodesColumnListModel, in *genmodel.GenDaoConfig) {
field.GoName, field.GoType, field.TsName, field.TsType = GenGotype(ctx, field, in)
setDefault(field)
}
func TableColumnList(ctx context.Context, params *genmodel.GenCodesColumnListParam) (output []*genmodel.GenCodesColumnListModel, err error) {
sql := "select ORDINAL_POSITION as `id`, COLUMN_NAME as `name`, COLUMN_COMMENT as `dc`, DATA_TYPE as `dataType`, COLUMN_TYPE as `sqlType`, CHARACTER_MAXIMUM_LENGTH as `length`, IS_NULLABLE as `isAllowNull`, COLUMN_DEFAULT as `defaultValue`, COLUMN_KEY as `index`, EXTRA as `extra` from information_schema.COLUMNS where TABLE_SCHEMA = ? and TABLE_NAME = ? ORDER BY `id` ASC"
dbConfig := g.DB(params.DbGroup).GetConfig()
daoConfig, err := gendao.GetDaoConfig(ctx, params.DbGroup)
if err != nil {
return
}
output = make([]*genmodel.GenCodesColumnListModel, 0)
err = g.DB(params.DbGroup).Ctx(ctx).Raw(sql, dbConfig.Name, params.TableName).Scan(&output)
if err != nil {
return
}
if len(output) == 0 {
return
}
for _, v := range output {
if params.IsLink == 1 {
CustomLinkAttributes(ctx, params.Alias, v, daoConfig)
} else {
CustomAttributes(ctx, v, daoConfig)
}
}
return
}
@@ -0,0 +1,341 @@
// package genview
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genview
import (
genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts"
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
"github.com/gogf/gf/v2/text/gstr"
)
// 默认表单组件映射 Ts -> 表单组件
var defaultFormModeMap = map[string]string{
TsTypeString: FMInput,
TsTypeNumber: FMInputNumber,
TsTypeBoolean: FMInputNumber,
TsTypeArray: FMInputDynamic,
TsTypeTuple: FMInputDynamic,
TsTypeAny: FMInput,
}
var defaultEditFields = map[string]bool{
"id": false,
"created_dept": false,
"created_by": false,
"created_at": false,
"updated_by": false,
"updated_at": false,
"deleted_by": false,
"deleted_at": false,
}
var defaultEditSwitch = map[string]bool{
"id": false,
"level": false,
"tree": false,
"created_by": false,
"updated_by": false,
"created_dept": false,
"created_at": false,
"updated_at": false,
"deleted_at": false,
}
var defaultListSwitch = map[string]bool{
"level": false,
"tree": false,
"deleted_at": false,
}
var defaultExportSwitch = map[string]bool{
"level": false,
"tree": false,
"deleted_at": false,
}
var defaultQuerySwitch = map[string]bool{
"level": false,
"tree": false,
"deleted_at": false,
}
var defaultSort = map[string]bool{
"id": true,
"sort": true,
}
var defaultTreeFields = []string{"pid", "level", "tree"}
// 默认表单验证映射 物理类型命名识别
var defaultFormRoleMap = map[string]string{
"mobile": FRPhone,
"qq": FRQq,
"email": FREmail,
"id_card": FRIdCard,
"bank_card": FRBankCard,
"password": FRPassword,
"pass": FRPassword,
"price": FRAmount,
}
// 默认查询条件映射 go类型识别
var defaultWhereModeMap = map[string]string{
GoTypeString: WMLike,
GoTypeDate: WMEq,
GoTypeDatetime: WMEq,
GoTypeInt: WMEq,
GoTypeUint: WMEq,
GoTypeInt64: WMEq,
GoTypeUint64: WMEq,
GoTypeIntSlice: WMIn,
GoTypeInt64Slice: WMIn,
GoTypeUint64Slice: WMIn,
GoTypeFloat32: WMEq,
GoTypeFloat64: WMEq,
GoTypeBytes: WMEq,
GoTypeTime: WMEq,
GoTypeGTime: WMEq,
GoTypeJson: WMJsonContains,
}
// IsIndexPK 是否是主键
func IsIndexPK(index string) bool {
return gstr.ToUpper(index) == gstr.ToUpper(genconsts.GenCodesIndexPK)
}
// IsIndexUNI 是否是唯一索引
func IsIndexUNI(index string) bool {
return gstr.ToUpper(index) == gstr.ToUpper(genconsts.GenCodesIndexUNI)
}
// setDefault 设置默认属性
func setDefault(field *genmodel.GenCodesColumnListModel) {
setDefaultEdit(field)
setDefaultFormMode(field)
setDefaultFormRole(field)
setDefaultDictType(field)
setDefaultList(field)
setDefaultExport(field)
setDefaultQuery(field)
setDefaultQueryWhere(field)
setDefaultValue(field)
if field.IsAllowNull == "NO" {
field.Required = true
}
if IsIndexUNI(field.Index) {
field.Unique = true
}
if df, ok := defaultSort[field.Name]; ok {
field.IsSort = df
}
if field.Dc == "" {
field.Dc = field.Name
}
}
// setDefaultEdit 设置默认编辑
func setDefaultEdit(field *genmodel.GenCodesColumnListModel) {
field.IsEdit = true
if IsIndexPK(field.Index) {
field.IsEdit = false
return
}
if df, ok := defaultEditFields[field.Name]; ok {
field.IsEdit = df
}
if df, ok := defaultEditSwitch[field.Name]; ok {
field.IsEdit = df
}
}
// setDefaultFormMode 设置默认表单组件
func setDefaultFormMode(field *genmodel.GenCodesColumnListModel) {
field.FormMode = FMInput
if df, ok := defaultFormModeMap[field.TsType]; ok {
field.FormMode = df
}
if gstr.HasSuffix(field.GoName, "Status") && IsNumberType(field.GoType) {
field.FormMode = FMSelect
return
}
if field.GoName == "CreatedAt" {
field.FormMode = FMDateRange
return
}
if (field.GoName == "ProvinceId" || field.GoName == "CityId") && IsNumberType(field.GoType) {
field.FormMode = FMCitySelector
return
}
if field.DataType == "datetime" || field.DataType == "timestamp" || field.DataType == "timestamptz" {
field.FormMode = FMTime
return
}
if field.DataType == "date" {
field.FormMode = FMDate
return
}
if field.GoType == GoTypeString && field.Length >= 256 && field.Length <= 512 {
field.FormMode = FMInputTextarea
return
}
if field.GoType == GoTypeString && field.Length > 512 {
field.FormMode = FMInputEditor
return
}
}
// setDefaultFormRole 设置默认表单验证
func setDefaultFormRole(field *genmodel.GenCodesColumnListModel) {
field.FormRole = FRNone
switch field.GoType {
case GoTypeUint, GoTypeUint64:
field.FormRole = FRNum
return
}
if df, ok := defaultFormRoleMap[field.Name]; ok {
field.FormRole = df
}
}
// setDefaultDictType 设置默认字典类型
func setDefaultDictType(field *genmodel.GenCodesColumnListModel) {
if gstr.HasSuffix(field.GoName, "Status") && IsNumberType(field.GoType) {
field.DictType = "sys_common_status" // 默认系统状态ID
return
}
}
// setDefaultList 设置默认列表
func setDefaultList(field *genmodel.GenCodesColumnListModel) {
field.IsList = true
switch field.GoType {
case GoTypeIntSlice, GoTypeInt64Slice, GoTypeUint64Slice, GoTypeBytes, GoTypeJson:
field.IsList = false
return
}
if field.Length >= 500 {
field.IsList = false
return
}
if df, ok := defaultListSwitch[field.Name]; ok {
field.IsList = df
}
}
// setDefaultExport 设置默认导出
func setDefaultExport(field *genmodel.GenCodesColumnListModel) {
field.IsExport = true
switch field.GoType {
case GoTypeIntSlice, GoTypeInt64Slice, GoTypeUint64Slice, GoTypeBytes, GoTypeJson:
field.IsExport = false
return
}
if field.Length >= 500 {
field.IsExport = false
return
}
if df, ok := defaultExportSwitch[field.Name]; ok {
field.IsExport = df
}
}
// setDefaultQuery 设置默认查询
func setDefaultQuery(field *genmodel.GenCodesColumnListModel) {
field.IsQuery = false
if IsIndexPK(field.Index) {
field.IsQuery = true
return
}
if gstr.HasSuffix(field.GoName, "Status") && IsNumberType(field.GoType) {
field.IsQuery = true
return
}
if field.GoName == "CreatedAt" {
field.IsQuery = true
return
}
if df, ok := defaultQuerySwitch[field.Name]; ok {
field.IsQuery = df
}
}
// setDefaultQueryWhere 设置默认查询条件
func setDefaultQueryWhere(field *genmodel.GenCodesColumnListModel) {
field.QueryWhere = WMEq
if field.GoName == "CreatedAt" {
field.QueryWhere = WMBetween
return
}
if field.Length >= 500 {
field.QueryWhere = WMLikeAll
return
}
if df, ok := defaultWhereModeMap[field.GoType]; ok {
field.QueryWhere = df
}
}
// setDefaultValue 设置默认value
func setDefaultValue(field *genmodel.GenCodesColumnListModel) {
var value interface{}
if field.DefaultValue == nil {
switch field.GoType {
case GoTypeString, GoTypeBytes, GoTypeDate, GoTypeDatetime, GoTypeTime, GoTypeGTime:
value = ""
case GoTypeIntSlice, GoTypeInt64Slice, GoTypeUint64Slice, GoTypeJson:
value = nil
case GoTypeInt, GoTypeUint, GoTypeInt64, GoTypeUint64:
value = 0
case GoTypeBool:
value = false
}
} else {
value = genconsts.ConvType(field.DefaultValue, field.GoType)
}
// 时间类型不做默认值处理
if field.GoType == GoTypeGTime {
value = ""
}
field.DefaultValue = value
}
@@ -0,0 +1,339 @@
// package genview
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genview
import (
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
"github.com/gogf/gf/v2/text/gstr"
)
// 表单组件
const (
FMInput = "Input" // 文本输入
FMInputNumber = "InputNumber" // 数字输入
FMInputTextarea = "InputTextarea" // 文本域
FMInputEditor = "InputEditor" // 富文本
FMInputDynamic = "InputDynamic" // 动态键值对
FMDate = "Date" // 日期选择(Y-M-D)
FMDateRange = "DateRange" // 日期范围选择
FMTime = "Time" // 时间选择(Y-M-D H:i:s)
FMTimeRange = "TimeRange" // 时间范围选择
FMRadio = "Radio" // 单选按钮
FMCheckbox = "Checkbox" // 复选按钮
FMSelect = "Select" // 单选下拉框
FMSelectMultiple = "SelectMultiple" // 多选下拉框
FMTreeSelect = "TreeSelect" // 树型选择
FMCascader = "Cascader" // 级联选择
FMUploadImage = "UploadImage" // 单图上传
FMUploadImages = "UploadImages" // 多图上传
FMUploadFile = "UploadFile" // 单文件上传
FMUploadFiles = "UploadFiles" // 多文件上传
FMSwitch = "Switch" // 开关
FMRate = "Rate" // 评分
FMCitySelector = "CitySelector" // 省市区选择
FMPidTreeSelect = "PidTreeSelect" // 树型上级选择,树表生成专用
)
var FMs = []string{
FMInput, FMInputNumber, FMInputTextarea, FMInputEditor, FMInputDynamic,
FMDate, FMDateRange, FMTime, FMTimeRange,
FMRadio, FMCheckbox, FMSelect, FMSelectMultiple, FMTreeSelect, FMCascader,
FMUploadImage, FMUploadImages, FMUploadFile, FMUploadFiles,
FMSwitch,
FMRate,
FMCitySelector,
}
var FMMap = map[string]string{
FMInput: "文本输入",
FMInputNumber: "数字输入",
FMInputTextarea: "文本域",
FMInputEditor: "富文本",
FMInputDynamic: "动态键值对",
FMDate: "日期选择(Y-M-D)",
FMDateRange: "日期范围选择",
FMTime: "时间选择(Y-M-D H:i:s)",
FMTimeRange: "时间范围选择",
FMRadio: "单选按钮",
FMCheckbox: "复选按钮",
FMSelect: "单选下拉框",
FMSelectMultiple: "多选下拉框",
FMTreeSelect: "树型选择",
FMCascader: "级联选择",
FMUploadImage: "单图上传",
FMUploadImages: "多图上传",
FMUploadFile: "单文件上传",
FMUploadFiles: "多文件上传",
FMSwitch: "开关",
FMRate: "评分",
FMCitySelector: "省市区选择",
}
// 表单验证
const (
FRNone = "none"
FRIp = "ip"
FRPercentage = "percentage"
FRTel = "tel"
FRPhone = "phone"
FRQq = "qq"
FREmail = "email"
FRIdCard = "idCard"
FRNum = "num"
FRBankCard = "bankCard"
FRWeibo = "weibo"
FRUserName = "userName"
FRAccount = "account"
FRPassword = "password"
FRAmount = "amount"
)
var FRMap = map[string]string{
FRNone: "不验证",
FRIp: "Ipv4或Ipv6",
FRPercentage: "0-100百分比",
FRTel: "固话格式",
FRPhone: "手机号",
FRQq: "QQ号码",
FREmail: "邮箱",
FRIdCard: "身份证",
FRNum: "非零正整数",
FRBankCard: "银行卡",
FRWeibo: "微博号",
FRUserName: "用户名",
FRAccount: "账号",
FRPassword: "密码",
FRAmount: "金额",
}
// 查询条件
const (
WMEq = "=" // =
WMNeq = "!=" // !=
WMGt = ">" // >
WMGte = ">=" // >=
WMLt = "<" // <
WMLte = "<=" // <=
WMIn = "IN" // IN (...)
WMNotIn = "NOT IN" // NOT IN (...)
WMBetween = "BETWEEN" // BETWEEN
WMNotBetween = "NOT BETWEEN" // NOT BETWEEN
WMLike = "LIKE" // LIKE
WMLikeAll = "LIKE %...%" // LIKE %...%
WMNotLike = "NOT LIKE" // NOT LIKE
WMJsonContains = "JSON_CONTAINS(doc, val)" // JSON_CONTAINS(json_doc, val[, path]) // 判断是否包含某个json值
)
var WMs = []string{WMEq,
WMNeq, WMGt, WMGte, WMLt, WMLte,
WMIn, WMNotIn,
WMBetween, WMNotBetween,
WMLike, WMLikeAll, WMNotLike,
WMJsonContains,
}
// 表格列的排序方式
const (
TableAlignLeft = "left"
TableAlignRight = "right"
TableAlignCenter = "center"
)
var TableAligns = []string{TableAlignLeft, TableAlignRight, TableAlignCenter}
var TableAlignMap = map[string]string{
TableAlignLeft: "居左",
TableAlignRight: "居右",
TableAlignCenter: "居中",
}
// 是否是数字类型
func IsNumberType(goType string) bool {
switch goType {
case GoTypeInt, GoTypeUint, GoTypeInt64, GoTypeUint64:
return true
case GoTypeFloat32, GoTypeFloat64:
return true
}
return false
}
// IsSelectFormModel 是否是选择器组件
func IsSelectFormModel(FM string) bool {
switch FM {
case FMRadio, FMCheckbox, FMSelect, FMSelectMultiple, FMCitySelector, FMTreeSelect, FMCascader:
return true
}
return false
}
// 字段映射关系
// go类型
const (
GoTypeString = "string"
GoTypeDate = "date"
GoTypeDatetime = "datetime"
GoTypeInt = "int"
GoTypeUint = "uint"
GoTypeInt64 = "int64"
GoTypeUint64 = "uint64"
GoTypeIntSlice = "[]int"
GoTypeInt64Slice = "[]int64"
GoTypeUint64Slice = "[]uint64"
GoTypeFloat32 = "float32"
GoTypeFloat64 = "float64"
GoTypeBytes = "[]byte"
GoTypeBool = "bool"
GoTypeTime = "time.Time"
GoTypeGTime = "*gtime.Time"
GoTypeJson = "*gjson.Json"
)
var GoTypeNameMap = map[string]string{
GoTypeString: GoTypeString,
GoTypeDate: GoTypeDate,
GoTypeDatetime: GoTypeDatetime,
GoTypeInt: GoTypeInt,
GoTypeUint: GoTypeUint,
GoTypeInt64: GoTypeInt64,
GoTypeUint64: GoTypeUint64,
GoTypeIntSlice: GoTypeIntSlice,
GoTypeInt64Slice: GoTypeInt64Slice,
GoTypeUint64Slice: GoTypeUint64Slice,
GoTypeFloat32: GoTypeFloat32,
GoTypeFloat64: GoTypeFloat64,
GoTypeBytes: GoTypeBytes,
GoTypeBool: GoTypeBool,
GoTypeTime: GoTypeTime,
GoTypeGTime: GoTypeGTime,
GoTypeJson: GoTypeJson,
}
// ts类型
const (
TsTypeString = "string"
TsTypeNumber = "number"
TsTypeBoolean = "boolean"
TsTypeArray = "array"
TsTypeTuple = "tuple"
TsTypeAny = "any"
)
var TsTypeNameMap = map[string]string{
TsTypeString: TsTypeString,
TsTypeNumber: TsTypeNumber,
TsTypeBoolean: TsTypeBoolean,
TsTypeArray: TsTypeArray,
TsTypeTuple: TsTypeTuple,
TsTypeAny: TsTypeAny,
}
// ShiftMap Go -> Ts 类型转换
var ShiftMap = map[string]string{
GoTypeString: TsTypeString,
GoTypeDate: TsTypeString,
GoTypeDatetime: TsTypeString,
GoTypeInt: TsTypeNumber,
GoTypeUint: TsTypeNumber,
GoTypeInt64: TsTypeNumber,
GoTypeUint64: TsTypeNumber,
GoTypeIntSlice: TsTypeArray,
GoTypeInt64Slice: TsTypeArray,
GoTypeUint64Slice: TsTypeArray,
GoTypeFloat32: TsTypeNumber,
GoTypeFloat64: TsTypeNumber,
GoTypeBytes: TsTypeString,
GoTypeBool: TsTypeBoolean,
GoTypeTime: TsTypeString,
GoTypeGTime: TsTypeString,
GoTypeJson: TsTypeAny,
}
func HasColumn(fields []*genmodel.GenCodesColumnListModel, column string) bool {
for _, field := range fields {
if field.GoName == column {
return true
}
}
return false
}
func HasColumnWithFormMode(fields []*genmodel.GenCodesColumnListModel, formMode string) bool {
for _, field := range fields {
if field.FormMode == formMode {
return true
}
}
return false
}
func HasMaxSort(fields []*genmodel.GenCodesColumnListModel) bool {
return HasColumn(fields, "Sort")
}
func HasStatus(headOps []string, fields []*genmodel.GenCodesColumnListModel) bool {
if !gstr.InArray(headOps, "status") {
return false
}
return HasColumn(fields, "Status")
}
func HasSwitch(fields []*genmodel.GenCodesColumnListModel) bool {
return HasColumnWithFormMode(fields, FMSwitch)
}
func HasHookMemberSummary(fields []*genmodel.GenCodesColumnListModel) bool {
for _, field := range fields {
if IsMemberSummaryField(field.Name) {
if field.IsList {
return true
}
}
}
return false
}
func HasQueryMemberSummary(fields []*genmodel.GenCodesColumnListModel) bool {
for _, field := range fields {
if IsMemberSummaryField(field.Name) {
if field.IsQuery {
return true
}
}
}
return false
}
func IsMemberSummaryField(name string) bool {
switch name {
case "created_by", "updated_by", "deleted_by":
return true
}
return false
}
// ReviseFields 校正字段值,兼容版本升级前的老数据格式
func ReviseFields(fields []*genmodel.GenCodesColumnListModel) []*genmodel.GenCodesColumnListModel {
for _, field := range fields {
if !gstr.InArray(TableAligns, field.Align) {
field.Align = TableAlignLeft
}
if field.Width < 1 {
field.Width = -1
}
if field.Width > 2000 {
field.Width = 2000
}
if field.FormGridSpan < 1 {
field.FormGridSpan = 1
}
}
return fields
}
@@ -0,0 +1,993 @@
// package genview
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genview
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts"
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
"git.5iot.com/iotlcm-public/iotlcm-app/utility/tree"
version "git.5iot.com/iotlcm-public/iotlcm-app/utility/version"
"runtime"
"strings"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/os/gview"
"github.com/gogf/gf/v2/text/gstr"
)
var Curd = gCurd{}
type gCurd struct{}
func (l *gCurd) DoBuild(ctx context.Context, in *genmodel.CurdBuildParam) (err error) {
st := gtime.Now()
preview, err := l.DoPreview(ctx, in.PreviewIn)
if err != nil {
return
}
db, err := g.DB().Open(ParseDBConfigNodeLink(&gdb.ConfigNode{Link: in.PreviewIn.DaoConfig.Link}))
if err != nil {
err = gerror.Newf("连接数据库失败,请检查配置文件[server/hack/config.yaml]数据库配置是否正确!err:%v", err.Error())
return err
}
defer db.Close()
if err = db.Ping(); err != nil {
err = gerror.Newf("数据库访问异常,请检查配置文件[server/hack/config.yaml]数据库配置是否正确!err:%v", err.Error())
return
}
// 前置操作
if len(in.BeforeEvent) > 0 {
for name, f := range in.BeforeEvent {
if gstr.InArray(in.PreviewIn.Options.AutoOps, name) {
if err = f(ctx); err != nil {
return gerror.Newf("in doBuild operation beforeEvent to '%s' failed:%v", name, err)
}
}
}
}
// 处理sql文件
handleSqlFile := func(vi *genmodel.GenFile) (err error) {
// 无需生成
if vi.Meth != genconsts.GenCodesBuildMethCreate && vi.Meth != genconsts.GenCodesBuildMethCover {
return
}
if err = gfile.PutContents(vi.Path, strings.TrimSpace(vi.Content)); err != nil {
return gerror.Newf("writing content to '%s' failed: %v", vi.Path, err)
}
// 导入失败,将sql文件删除
if err = ImportSql(ctx, vi.Path); err != nil {
_ = gfile.Remove(vi.Path)
}
return
}
// 将sql文件提取出来优先处理
// sql执行过程出错是高概率事件,后期在执行前要进行预效验,尽量减少在执行过程中出错的可能性
sqlGenFile, ok := preview.Views["source.sql"]
if ok {
delete(preview.Views, "source.sql")
if err = handleSqlFile(sqlGenFile); err != nil {
return
}
}
for _, vi := range preview.Views {
// 无需生成
if vi.Meth != genconsts.GenCodesBuildMethCreate && vi.Meth != genconsts.GenCodesBuildMethCover {
continue
}
if err = gfile.PutContents(vi.Path, strings.TrimSpace(vi.Content)); err != nil {
return gerror.Newf("writing content to '%s' failed: %v", vi.Path, err)
}
}
// 后置操作
if len(in.AfterEvent) > 0 {
for name, f := range in.AfterEvent {
if gstr.InArray(in.PreviewIn.Options.AutoOps, name) {
if err = f(ctx); err != nil {
return gerror.Newf("in doBuild operation afterEvent to '%s' failed:%v", name, err)
}
}
}
}
g.Log().Debugf(ctx, "generate code operation completed, %vms", gtime.Now().Sub(st).Milliseconds())
return
}
func (l *gCurd) DoPreview(ctx context.Context, in *genmodel.CurdPreviewParam) (res *genmodel.GenCodesPreviewModel, err error) {
// 初始化
if err = l.initInput(ctx, in); err != nil {
return nil, err
}
// 加载模板
if err = l.loadView(ctx, in); err != nil {
return nil, err
}
if err = l.generateApiContent(ctx, in); err != nil {
return nil, err
}
if err = l.generateInputContent(ctx, in); err != nil {
return nil, err
}
if err = l.generateControllerContent(ctx, in); err != nil {
return nil, err
}
if err = l.generateLogicContent(ctx, in); err != nil {
return nil, err
}
if err = l.generateRouterContent(ctx, in); err != nil {
return nil, err
}
if err = l.generateWebApiContent(ctx, in); err != nil {
return nil, err
}
if err = l.generateWebModelContent(ctx, in); err != nil {
return nil, err
}
if err = l.generateWebIndexContent(ctx, in); err != nil {
return nil, err
}
if err = l.generateWebEditContent(ctx, in); err != nil {
return nil, err
}
if err = l.generateWebViewContent(ctx, in); err != nil {
return nil, err
}
if err = l.generateSqlContent(ctx, in); err != nil {
return nil, err
}
in.Content.Config = in.Config
res = in.Content
return
}
func (l *gCurd) initInput(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
in.Content = new(genmodel.GenCodesPreviewModel)
in.Content.Views = make(map[string]*genmodel.GenFile)
// 初始化生成选项
if err = initOptions(in); err != nil {
return err
}
// 初始化表字段配置
if err = initTableField(ctx, in); err != nil {
return err
}
// 初始化树表
if err = initTableTree(in); err != nil {
return err
}
// 初始化生成选型
initStep(in)
// 初始化方法字典
if err = initFuncDict(in); err != nil {
return err
}
// 初始化生成模板
if err = initTemplate(in); err != nil {
return err
}
return
}
func initOptions(in *genmodel.CurdPreviewParam) (err error) {
if err = in.In.Options.Scan(&in.Options); err != nil {
return
}
in.Options.DictMap = make(g.Map)
return
}
func initTableField(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
// 加载主表配置
if err = in.In.MasterColumns.Scan(&in.MasterFields); err != nil {
return
}
if len(in.MasterFields) == 0 {
if in.MasterFields, err = TableColumnList(ctx, &genmodel.GenCodesColumnListParam{DbGroup: in.In.DbName, TableName: in.In.TableName}); err != nil {
return
}
}
// 主键属性
in.Pk = getPkField(in)
if in.Pk == nil {
return gerror.New("initInput no primary key is set in the table!")
}
in.MasterFields = ReviseFields(in.MasterFields)
// 检查表命名
var names = []string{in.In.DaoName}
for _, v := range in.Options.Join {
v.Columns = ReviseFields(v.Columns)
names = append(names, v.DaoName)
g.Log().Infof(ctx, "initTableField in.Options.Join: %+v", v)
}
if err = CheckIllegalName("数据库表名", names...); err != nil {
return
}
if err = CheckIllegalName("实体命名", in.In.VarName); err != nil {
return
}
return
}
// getPkField 获取主键
func getPkField(in *genmodel.CurdPreviewParam) *genmodel.GenCodesColumnListModel {
if len(in.MasterFields) == 0 {
panic("getPkField masterFields uninitialized.")
}
for _, field := range in.MasterFields {
if IsIndexPK(field.Index) {
return field
}
}
return nil
}
func initTableTree(in *genmodel.CurdPreviewParam) (err error) {
// 检查树表字段
if in.In.GenType == genconsts.GenCodesTypeTree {
if err = CheckTreeTableFields(in.MasterFields); err != nil {
return err
}
if in.Options.Tree == nil {
in.Options.Tree = &genmodel.OptionsTree{}
}
if in.Options.Tree.PidColumn == "" {
in.Options.Tree.PidColumn = "parent_id"
}
if in.Options.Tree.TitleColumn == "" {
if field := inferTreeTitleField(in.MasterFields, in.Options.Tree.PidColumn); field != nil {
in.Options.Tree.TitleColumn = field.Name
}
}
hasTitle := false
hasPid := false
hasLevel := false
hasTree := false
for _, field := range in.MasterFields {
if in.Options.Tree.TitleColumn == field.Name {
in.Options.Tree.TitleField = field
hasTitle = true
}
if in.Options.Tree.PidColumn == field.Name {
in.Options.Tree.PidField = field
hasPid = true
}
if in.Options.Tree.LevelColumn != "" && in.Options.Tree.LevelColumn == field.Name {
in.Options.Tree.LevelField = field
hasLevel = true
}
if in.Options.Tree.TreeColumn != "" && in.Options.Tree.TreeColumn == field.Name {
in.Options.Tree.TreeField = field
hasTree = true
}
}
if !hasTitle {
err = gerror.New("请选择一个有效的树名称字段")
return
}
if !hasPid {
err = gerror.New("请选择一个有效的父级字段")
return
}
in.Options.Tree.HasTreePath = hasLevel && hasTree
}
return err
}
func inferTreeTitleField(fields []*genmodel.GenCodesColumnListModel, pidColumn string) *genmodel.GenCodesColumnListModel {
preferredNames := []string{"tree_name", "name", "title", "label", "menu_name", "dept_name", "category_name"}
for _, name := range preferredNames {
for _, field := range fields {
if field.Name == name {
return field
}
}
}
for _, field := range fields {
if IsIndexPK(field.Index) || field.Name == pidColumn || field.Name == "tenant_id" {
continue
}
goType := strings.TrimPrefix(field.GoType, "*")
if goType == "string" {
return field
}
}
return nil
}
// CheckTreeTableFields 检查树表字段
func CheckTreeTableFields(columns []*genmodel.GenCodesColumnListModel) (err error) {
return
}
func initStep(in *genmodel.CurdPreviewParam) {
in.Options.Step = new(genmodel.CurdStep)
in.Options.Step.HasMaxSort = HasMaxSort(in.MasterFields)
in.Options.Step.HasAdd = gstr.InArray(in.Options.HeadOps, "add")
in.Options.Step.HasBatchDel = gstr.InArray(in.Options.HeadOps, "batchDel") && gstr.InArray(in.Options.ColumnOps, "check")
in.Options.Step.HasExport = gstr.InArray(in.Options.HeadOps, "export")
in.Options.Step.HasNotFilterAuth = gstr.InArray(in.Options.ColumnOps, "notFilterAuth")
in.Options.Step.HasEdit = gstr.InArray(in.Options.ColumnOps, "edit")
in.Options.Step.HasDel = gstr.InArray(in.Options.ColumnOps, "del")
in.Options.Step.HasView = gstr.InArray(in.Options.ColumnOps, "view")
in.Options.Step.HasStatus = HasStatus(in.Options.ColumnOps, in.MasterFields)
in.Options.Step.HasSwitch = HasSwitch(in.MasterFields)
in.Options.Step.HasCheck = gstr.InArray(in.Options.ColumnOps, "check")
in.Options.Step.HasMenu = gstr.InArray(in.Options.AutoOps, "genMenuPermissions")
in.Options.Step.HasQueryMemberSummary = HasQueryMemberSummary(in.MasterFields)
in.Options.Step.HasHookMemberSummary = HasHookMemberSummary(in.MasterFields)
in.Options.Step.IsTreeTable = in.In.GenType == genconsts.GenCodesTypeTree
if in.Options.Step.IsTreeTable {
in.Options.Step.IsOptionTreeTable = in.Options.Tree.StyleType == genconsts.GenCodesTreeStyleTypeOption
}
in.Options.Step.HasFuncDict = gstr.InArray(in.Options.AutoOps, "genFuncDict")
in.Options.Step.IsAddon = in.Config.Application.Crud.Templates[in.In.GenTemplate].IsAddon
if in.Options.PresetStep.FormGridCols < 1 {
in.Options.PresetStep.FormGridCols = 1
}
in.Options.Step.IsEditModal = gstr.InArray(in.Options.AutoOps, "genEditModal")
}
func initFuncDict(in *genmodel.CurdPreviewParam) (err error) {
g.Log().Infof(context.Background(), "server/internal/library/xgen/gen_curd.go initFuncDict in.Options.Step.HasFuncDict:%v in.Options.FuncDict:%+v", in.Options.Step.HasFuncDict, in.Options.FuncDict)
if !in.Options.Step.HasFuncDict || in.Options.FuncDict == nil {
return
}
if len(in.Options.FuncDict.LabelColumn) == 0 || len(in.Options.FuncDict.ValueColumn) == 0 {
err = gerror.New("生成字典选项必须设置选项值和选项名称")
return err
}
for _, field := range in.MasterFields {
if field.Name == in.Options.FuncDict.ValueColumn {
in.Options.FuncDict.Value = field
}
if field.Name == in.Options.FuncDict.LabelColumn {
in.Options.FuncDict.Label = field
}
}
g.Log().Infof(context.Background(), "server/internal/library/xgen/gen_curd.go initFuncDict in.Options.Step.HasFuncDict:%v in.Options.FuncDict:%+v", in.Options.Step.HasFuncDict, in.Options.FuncDict)
return
}
func initTemplate(in *genmodel.CurdPreviewParam) (err error) {
if len(in.Config.Application.Crud.Templates)-1 < in.In.GenTemplate {
return gerror.New("没有找到生成模板的配置,请检查!")
}
// api前缀
apiPrefix := gstr.LcFirst(in.In.VarName)
if in.Config.Application.Crud.Templates[in.In.GenTemplate].IsAddon {
apiPrefix = in.In.AddonName + "/" + apiPrefix
}
in.Options.ApiPrefix = apiPrefix
if err = checkCurdPath(in.Config.Application.Crud.Templates[in.In.GenTemplate], in.In.AddonName); err != nil {
return
}
in.Options.TemplateGroup = in.Config.Application.Crud.Templates[in.In.GenTemplate].MasterPackage
return
}
func checkCurdPath(temp *genmodel.GenCodesConfigCrudTemplate, addonName string) (err error) {
if temp == nil {
return gerror.New("生成模板配置不能为空")
}
if temp.IsAddon {
temp.TemplatePath = gstr.Replace(temp.TemplatePath, "{$name}", addonName)
temp.ApiPath = gstr.Replace(temp.ApiPath, "{$name}", addonName)
temp.InputPath = gstr.Replace(temp.InputPath, "{$name}", addonName)
temp.ControllerPath = gstr.Replace(temp.ControllerPath, "{$name}", addonName)
temp.LogicPath = gstr.Replace(temp.LogicPath, "{$name}", addonName)
temp.RouterPath = gstr.Replace(temp.RouterPath, "{$name}", addonName)
temp.SqlPath = gstr.Replace(temp.SqlPath, "{$name}", addonName)
temp.WebApiPath = gstr.Replace(temp.WebApiPath, "{$name}", addonName)
temp.WebViewsPath = gstr.Replace(temp.WebViewsPath, "{$name}", addonName)
}
tip := `生成模板配置参数'%s'路径不存在,请先创建路径:%s`
if !gfile.Exists(temp.TemplatePath) {
return gerror.Newf(tip, "TemplatePath", temp.TemplatePath)
}
if !gfile.Exists(temp.ApiPath) {
return gerror.Newf(tip, "ApiPath", temp.ApiPath)
}
if !gfile.Exists(temp.InputPath) {
return gerror.Newf(tip, "InputPath", temp.InputPath)
}
if !gfile.Exists(temp.ControllerPath) {
return gerror.Newf(tip, "ControllerPath", temp.ControllerPath)
}
if !gfile.Exists(temp.LogicPath) {
return gerror.Newf(tip, "LogicPath", temp.LogicPath)
}
if !gfile.Exists(temp.RouterPath) {
return gerror.Newf(tip, "RouterPath", temp.RouterPath)
}
if !gfile.Exists(temp.SqlPath) {
return gerror.Newf(tip, "SqlPath", temp.SqlPath)
}
if !gfile.Exists(temp.WebApiPath) {
return gerror.Newf(tip, "WebApiPath", temp.WebApiPath)
}
if !gfile.Exists(temp.WebViewsPath) {
return gerror.Newf(tip, "WebViewsPath", temp.WebViewsPath)
}
return
}
func (l *gCurd) loadView(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
temp := in.Config.Application.Crud.Templates[in.In.GenTemplate]
view := gview.New()
err = view.SetConfigWithMap(g.Map{
"Paths": temp.TemplatePath,
"Delimiters": in.Config.Delimiters,
})
if err != nil {
return
}
now := gtime.Now()
view.BindFuncMap(g.Map{
"NowYear": now.Year, // 当前年
"ToLower": strings.ToLower, // 全部小写
"LcFirst": gstr.LcFirst, // 首字母小写
"UcFirst": gstr.UcFirst, // 首字母大写
"ToTSArray": ToTSArray, // 转为ts数组格式
"DictToTSArray": DictToTSArray, // 转为ts数组格式
})
if err = l.generateWebModelDictOptions(ctx, in); err != nil {
return
}
modName, err := GetModName(ctx)
if err != nil {
return
}
importApi := gstr.Replace(temp.ApiPath, "./", modName+"/") + "/" + strings.ToLower(in.In.VarName) + "/v1"
importInput := gstr.Replace(temp.InputPath, "./", modName+"/")
importController := gstr.Replace(temp.ControllerPath, "./", modName+"/")
importService := "git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
if temp.IsAddon {
importService = "git.5iot.com/iotlcm-public/iotlcm-app/addons/" + in.In.AddonName + "/service"
}
in.Options.ImportWebApi = "#/api/gen/" + gstr.LcFirst(in.In.VarName)
if temp.IsAddon {
in.Options.ImportWebApi = "#/api/gen/addons/" + in.In.AddonName + "/" + gstr.LcFirst(in.In.VarName)
}
componentPrefix := gstr.LcFirst(in.In.VarName)
if temp.IsAddon {
componentPrefix = "addons/" + in.In.AddonName + "/" + componentPrefix
}
nowTime := now.Format("Y-m-d H:i:s")
view.Assigns(gview.Params{
"templateGroup": in.Options.TemplateGroup, // 生成模板分组名称
"servFunName": l.parseServFunName(in.Options.TemplateGroup, in.In.VarName), // 业务服务名称
"nowTime": nowTime, // 当前时间
"version": runtime.Version(), // GO 版本
"xjVersion": version.BuildVersion, // XJ 版本
"varName": in.In.VarName, // 实体名称
"tableComment": in.In.TableComment, // 对外名称
"daoName": in.In.DaoName, // ORM模型
"masterFields": in.MasterFields, // 主表字段
"pk": in.Pk, // 主键属性
"options": in.Options, // 提交选项
"dictOptions": in.Options.DictOps, // web字典选项
"importApi": importApi, // 导入goApi包
"importInput": importInput, // 导入input包
"importController": importController, // 导入控制器包
"importService": importService, // 导入业务服务
"importWebApi": in.Options.ImportWebApi, // 导入webApi
"apiPrefix": in.Options.ApiPrefix, // api前缀
"componentPrefix": componentPrefix, // vue子组件前缀
})
in.View = view
return
}
func (l *gCurd) generateApiContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
var (
name = "api.go"
tplData = g.Map{}
genFile = new(genmodel.GenFile)
)
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
if err != nil {
return err
}
genFile.Content, err = FormatGo(ctx, name, genFile.Content)
if err != nil {
return err
}
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].ApiPath, strings.ToLower(in.In.VarName), "v1", strings.ToLower(in.In.VarName)+".go")
genFile.Meth = genconsts.GenCodesBuildMethCreate
if gfile.Exists(genFile.Path) {
genFile.Meth = genconsts.GenCodesBuildMethSkip
}
genFile.Required = true
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
genFile.Meth = genconsts.GenCodesBuildMethCover
}
in.Content.Views[name] = genFile
return
}
func (l *gCurd) generateInputContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
var (
name = "input.go"
genFile = new(genmodel.GenFile)
)
tplData, err := l.inputTplData(ctx, in)
if err != nil {
return err
}
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
if err != nil {
return err
}
genFile.Content, err = FormatGo(ctx, name, genFile.Content)
if err != nil {
return err
}
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].InputPath, CamelCaseToUnderline(in.In.VarName)+".go")
genFile.Meth = genconsts.GenCodesBuildMethCreate
if gfile.Exists(genFile.Path) {
genFile.Meth = genconsts.GenCodesBuildMethSkip
}
genFile.Required = true
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
genFile.Meth = genconsts.GenCodesBuildMethCover
}
in.Content.Views[name] = genFile
return
}
func (l *gCurd) generateControllerContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
var (
name = "controller.go"
tplData = g.Map{}
genFile = new(genmodel.GenFile)
)
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
if err != nil {
return err
}
genFile.Content, err = FormatGo(ctx, name, genFile.Content)
if err != nil {
return err
}
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].ControllerPath, CamelCaseToUnderline(in.In.VarName)+".go")
genFile.Meth = genconsts.GenCodesBuildMethCreate
if gfile.Exists(genFile.Path) {
genFile.Meth = genconsts.GenCodesBuildMethSkip
}
genFile.Required = true
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
genFile.Meth = genconsts.GenCodesBuildMethCover
}
in.Content.Views[name] = genFile
return
}
func (l *gCurd) generateLogicContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
var (
name = "logic.go"
genFile = new(genmodel.GenFile)
)
tplData, err := l.logicTplData(ctx, in)
if err != nil {
return err
}
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
if err != nil {
return err
}
genFile.Content, err = FormatGo(ctx, name, genFile.Content)
if err != nil {
return err
}
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].LogicPath, CamelCaseToUnderline(in.In.VarName)+".go")
genFile.Meth = genconsts.GenCodesBuildMethCreate
if gfile.Exists(genFile.Path) {
genFile.Meth = genconsts.GenCodesBuildMethSkip
}
genFile.Required = true
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
genFile.Meth = genconsts.GenCodesBuildMethCover
}
in.Content.Views[name] = genFile
return
}
func (l *gCurd) generateRouterContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
var (
name = "router.go"
tplData = g.Map{}
genFile = new(genmodel.GenFile)
)
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
if err != nil {
return err
}
genFile.Content, err = FormatGo(ctx, name, genFile.Content)
if err != nil {
return err
}
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].RouterPath, CamelCaseToUnderline(in.In.VarName)+".go")
genFile.Meth = genconsts.GenCodesBuildMethCreate
if gfile.Exists(genFile.Path) {
genFile.Meth = genconsts.GenCodesBuildMethSkip
}
genFile.Required = true
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
genFile.Meth = genconsts.GenCodesBuildMethCover
}
in.Content.Views[name] = genFile
return
}
func (l *gCurd) generateWebApiContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
var (
name = "web.api.ts"
tplData = g.Map{}
genFile = new(genmodel.GenFile)
)
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
if err != nil {
return err
}
genFile.Content = FormatTs(genFile.Content)
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebApiPath, gstr.LcFirst(in.In.VarName), "index.ts")
genFile.Meth = genconsts.GenCodesBuildMethCreate
if gfile.Exists(genFile.Path) {
genFile.Meth = genconsts.GenCodesBuildMethSkip
}
genFile.Required = true
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
genFile.Meth = genconsts.GenCodesBuildMethCover
}
in.Content.Views[name] = genFile
return
}
func (l *gCurd) generateWebModelContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
var (
name = "web.model.ts"
genFile = new(genmodel.GenFile)
)
tplData, err := l.webModelTplData(ctx, in)
if err != nil {
return
}
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
if err != nil {
return
}
genFile.Content = FormatTs(genFile.Content)
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebViewsPath, gstr.LcFirst(in.In.VarName), "model.tsx")
genFile.Meth = genconsts.GenCodesBuildMethCreate
if gfile.Exists(genFile.Path) {
genFile.Meth = genconsts.GenCodesBuildMethCover
}
genFile.Required = true
in.Content.Views[name] = genFile
return
}
func (l *gCurd) generateWebIndexContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
var (
name = "web.index.vue"
genFile = new(genmodel.GenFile)
)
tplData, err := l.webIndexTplData(ctx, in)
if err != nil {
return err
}
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
if err != nil {
return err
}
genFile.Content = FormatVue(genFile.Content)
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebViewsPath, gstr.LcFirst(in.In.VarName), "index.vue")
genFile.Meth = genconsts.GenCodesBuildMethCreate
if gfile.Exists(genFile.Path) {
genFile.Meth = genconsts.GenCodesBuildMethSkip
}
genFile.Required = true
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
genFile.Meth = genconsts.GenCodesBuildMethCover
}
in.Content.Views[name] = genFile
return
}
func (l *gCurd) generateWebEditContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
var (
name = "web.edit.vue"
genFile = new(genmodel.GenFile)
)
tplData, err := l.webEditTplData(ctx, in)
if err != nil {
return err
}
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
if err != nil {
return err
}
genFile.Content = FormatVue(genFile.Content)
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebViewsPath, gstr.LcFirst(in.In.VarName), "edit.vue")
genFile.Meth = genconsts.GenCodesBuildMethCreate
if gfile.Exists(genFile.Path) {
genFile.Meth = genconsts.GenCodesBuildMethSkip
}
genFile.Required = true
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
genFile.Meth = genconsts.GenCodesBuildMethCover
}
if !in.Options.Step.HasEdit {
genFile.Meth = genconsts.GenCodesBuildIgnore
genFile.Required = false
}
in.Content.Views[name] = genFile
return
}
func (l *gCurd) generateWebViewContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
var (
name = "web.view.vue"
genFile = new(genmodel.GenFile)
)
tplData, err := l.webViewTplData(ctx, in)
if err != nil {
return err
}
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
if err != nil {
return err
}
genFile.Content = FormatVue(genFile.Content)
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebViewsPath, gstr.LcFirst(in.In.VarName), "view.vue")
genFile.Meth = genconsts.GenCodesBuildMethCreate
if gfile.Exists(genFile.Path) {
genFile.Meth = genconsts.GenCodesBuildMethSkip
}
genFile.Required = true
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
genFile.Meth = genconsts.GenCodesBuildMethCover
}
if !in.Options.Step.HasView {
genFile.Meth = genconsts.GenCodesBuildIgnore
genFile.Required = false
}
in.Content.Views[name] = genFile
return
}
func (l *gCurd) generateSqlContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
var (
name = "source.sql"
config = g.DB("default").GetConfig()
tplData = g.Map{
"dbName": config.Name,
"menuTable": config.Prefix + "sys_menu",
"roleMenuTable": config.Prefix + "sys_role_menu",
"mainComponent": "BasicLayout",
}
genFile = new(genmodel.GenFile)
)
menus, err := service.SysMenu().GetFastList(ctx)
if err != nil {
return err
}
tplData["dirPid"], tplData["dirLevel"], tplData["dirTree"], err = xgorm.AutoUpdateTree(ctx, &dao.SysMenu, 0, int64(in.Options.Menu.Pid), &xgorm.TreeFiledOption{
IdField: "menu_id",
PidField: "parent_id",
LevelField: "level",
TreeField: "tree",
})
if err != nil {
return err
}
tplData["listLevel"] = tplData["dirLevel"].(int) + 1
tplData["btnLevel"] = tplData["dirLevel"].(int) + 2
tplData["sortLevel"] = tplData["dirLevel"].(int) + 3
pageRedirect := ""
if in.Options.Menu.Pid > 0 {
tplData["mainComponent"] = "ParentLayout"
menu, ok := menus[int64(in.Options.Menu.Pid)]
if !ok {
err = gerror.New("选择的上级菜单不存在")
return
}
for _, id := range tree.GetIds(menu.Tree) {
if v, ok2 := menus[id]; ok2 {
if !gstr.HasSuffix(pageRedirect, "/") && !gstr.HasPrefix(v.Path, "/") {
pageRedirect += "/"
}
pageRedirect += v.Path
}
}
if !gstr.HasSuffix(pageRedirect, "/") && !gstr.HasPrefix(menu.Path, "/") {
pageRedirect += "/"
}
pageRedirect += menu.Path
}
pageRedirect += "/" + gstr.LcFirst(in.In.VarName) + "/index"
tplData["pageRedirect"] = pageRedirect
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].SqlPath, CamelCaseToUnderline(in.In.VarName)+"_menu.sql")
genFile.Meth = genconsts.GenCodesBuildMethCreate
if gfile.Exists(genFile.Path) {
genFile.Meth = genconsts.GenCodesBuildMethSkip
}
genFile.Required = true
if !in.Options.Step.HasMenu {
genFile.Meth = genconsts.GenCodesBuildIgnore
genFile.Required = false
}
// 需要生成时,检查菜单命名是否存在
if genFile.Meth == genconsts.GenCodesBuildMethCreate {
menuNamePrefix := gstr.LcFirst(in.In.VarName)
menuNames := []string{menuNamePrefix, menuNamePrefix + "Index"}
if in.Options.Step.HasEdit {
menuNames = append(menuNames, menuNamePrefix+"Edit")
menuNames = append(menuNames, menuNamePrefix+"View")
}
if in.Options.Step.HasView {
menuNames = append(menuNames, menuNamePrefix+"View")
}
if in.Options.Step.HasMaxSort {
menuNames = append(menuNames, menuNamePrefix+"MaxSort")
}
if in.Options.Step.HasDel {
menuNames = append(menuNames, menuNamePrefix+"Delete")
}
if in.Options.Step.HasStatus {
menuNames = append(menuNames, menuNamePrefix+"Status")
}
if in.Options.Step.HasSwitch {
menuNames = append(menuNames, menuNamePrefix+"Switch")
}
if in.Options.Step.HasExport {
menuNames = append(menuNames, menuNamePrefix+"Export")
}
if in.Options.Step.IsTreeTable {
menuNames = append(menuNames, menuNamePrefix+"TreeOption")
}
menuNames = utility.UniqueSlice(menuNames)
hasMenus, err := service.SysMenu().Model(ctx).Fields("path").WhereIn("path", menuNames).Array()
if err != nil {
return err
}
if len(hasMenus) > 0 {
err = gerror.Newf("要生成的菜单中有已存在的路由别名,请检查并删除:%v", strings.Join(gvar.New(hasMenus).Strings(), `、`))
return err
}
}
tplData["generatePath"] = genFile.Path
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
if err != nil {
return err
}
in.Content.Views[name] = genFile
return
}
@@ -0,0 +1,414 @@
// package genview
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genview
import (
"bytes"
"context"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
"strings"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/text/gstr"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
)
const (
InputTypeListInp = 1 // 列表输入
InputTypeListModel = 2 // 列表输出
InputTypeExportModel = 3 // 列表导出
InputTypeEditInpValidator = 4 // 添加&编辑验证器
InputTypeUpdateFields = 5 // 编辑修改过滤字段
InputTypeInsertFields = 6 // 编辑新增过滤字段
InputTypeTreeOptionFields = 7 // 关系树查询字段
InputTypeEditParamFields = 8 // 编辑参数过滤字段
EditInpValidatorGenerally = "if err := g.Validator().Rules(\"%s\").Data(in.%s).Messages(\"%s\").Run(ctx); err != nil {\n\t\treturn err.Current()\n\t}\n"
)
func (l *gCurd) inputTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) {
data = make(g.Map)
data["listInpColumns"] = l.genInputListColumns(ctx, in, InputTypeListInp)
data["listModelColumns"] = l.genInputListColumns(ctx, in, InputTypeListModel)
data["exportModelColumns"] = l.genInputListColumns(ctx, in, InputTypeExportModel)
data["editInpValidator"] = l.genInputListColumns(ctx, in, InputTypeEditInpValidator)
data["updateFieldsColumns"] = l.genInputListColumns(ctx, in, InputTypeUpdateFields)
data["insertFieldsColumns"] = l.genInputListColumns(ctx, in, InputTypeInsertFields)
data["editParamFieldsColumns"] = l.genInputListColumns(ctx, in, InputTypeEditParamFields)
data["viewModelColumns"] = l.genInputViewColumns(ctx, in)
if in.Options.Step.IsTreeTable {
data["treeOptionFields"] = l.genInputListColumns(ctx, in, InputTypeTreeOptionFields)
}
return
}
func (l *gCurd) genInputViewColumns(ctx context.Context, in *genmodel.CurdPreviewParam) string {
buffer := bytes.NewBuffer(nil)
index := 0
array := make([][]string, 1000)
// 主表
for _, field := range in.MasterFields {
// 查询用户摘要
if field.IsList && in.Options.Step.HasHookMemberSummary && IsMemberSummaryField(field.Name) {
tagKey := "`"
descriptionTag := gstr.Replace(formatComment(field.Dc)+"摘要信息", `"`, `\"`)
result := []string{" #" + field.GoName + "Summa"}
result = append(result, " #*hook.MemberSumma")
result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, field.TsName+"Summa"))
result = append(result, " #"+fmt.Sprintf(`dc:"%s"`+tagKey, descriptionTag))
array[index] = result
index++
}
}
table := tablewriter.NewTable(buffer)
table.Options(tablewriter.WithRendition(tw.Rendition{
Borders: tw.Border{
Top: tw.Fail,
Bottom: tw.Fail,
Left: tw.Success,
Right: tw.Success,
},
Settings: tw.Settings{
Lines: tw.Lines{
ShowTop: tw.Fail,
ShowBottom: tw.Fail,
ShowHeaderLine: tw.Fail,
ShowFooterLine: tw.Fail,
},
},
}), tablewriter.WithRowConfig(tw.CellConfig{
Formatting: tw.CellFormatting{
AutoWrap: tw.WrapNone,
},
}), tablewriter.WithSymbols(tw.NewSymbols(tw.StyleDefault)))
// tw.SetBorder(false)
// tw.SetRowLine(false)
// tw.SetAutoWrapText(false)
// tw.SetColumnSeparator("")
table.Bulk(array)
table.Render()
stContent := buffer.String()
// Let's do this hack of table writer for indent!
stContent = gstr.Replace(stContent, "│#", " ")
stContent = gstr.Replace(stContent, "│", " ")
stContent = gstr.Replace(stContent, " #", " ")
stContent = gstr.Replace(stContent, "` ", "`")
stContent = gstr.Replace(stContent, "``", "")
stContent = removeEndWrap(stContent)
buffer.Reset()
buffer.WriteString(stContent)
return "\tentity." + in.In.DaoName + "\n" + buffer.String()
}
func (l *gCurd) genInputListColumns(ctx context.Context, in *genmodel.CurdPreviewParam, inputType int) string {
buffer := bytes.NewBuffer(nil)
index := 0
array := make([][]string, 1000)
// 主表
for _, field := range in.MasterFields {
row := l.genStructFieldDefinition(in, field, inputType, true)
if row == nil {
continue
}
array[index] = row
index++
switch inputType {
case InputTypeListModel:
// 查询用户摘要
if field.IsList && in.Options.Step.HasHookMemberSummary && IsMemberSummaryField(field.Name) {
tagKey := "`"
descriptionTag := gstr.Replace(formatComment(field.Dc)+"摘要信息", `"`, `\"`)
result := []string{" #" + field.GoName + "Summa"}
result = append(result, " #*hook.MemberSumma")
result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, field.TsName+"Summa"))
result = append(result, " #"+fmt.Sprintf(`dc:"%s"`+tagKey, descriptionTag))
array[index] = result
index++
}
}
}
// 关联表
if len(in.Options.Join) > 0 {
for _, v := range in.Options.Join {
if !isEffectiveJoin(v) {
g.Log().Infof(ctx, "genInputListColumns isEffectiveJoin: %+v", v)
continue
}
g.Log().Infof(ctx, "genInputListColumns v: %+v", v)
for _, field := range v.Columns {
row := l.genStructFieldDefinition(in, field, inputType, false)
g.Log().Infof(ctx, "genInputListColumns field: %+v row: %+v", field, row)
if row != nil {
array[index] = row
index++
}
}
}
}
g.Log().Infof(ctx, "genInputListColumns in.Options.Join: %+v", in.Options.Join)
table := tablewriter.NewWriter(buffer)
table.Options(tablewriter.WithRendition(tw.Rendition{
Borders: tw.Border{
Top: tw.Fail,
Bottom: tw.Fail,
Left: tw.Success,
Right: tw.Success,
},
Settings: tw.Settings{
Lines: tw.Lines{
ShowTop: tw.Fail,
ShowBottom: tw.Fail,
ShowHeaderLine: tw.Fail,
ShowFooterLine: tw.Fail,
},
},
}), tablewriter.WithRowConfig(tw.CellConfig{
Formatting: tw.CellFormatting{
AutoWrap: tw.WrapNone,
},
}), tablewriter.WithSymbols(tw.NewSymbols(tw.StyleDefault)))
// tw.SetBorder(false)
// tw.SetRowLine(false)
// tw.SetAutoWrapText(false)
// tw.SetColumnSeparator("")
table.Bulk(array)
table.Render()
stContent := buffer.String()
// Let's do this hack of table writer for indent!
stContent = gstr.Replace(stContent, "│#", " ")
stContent = gstr.Replace(stContent, "│", " ")
stContent = gstr.Replace(stContent, " #", " ")
stContent = gstr.Replace(stContent, "` ", "`")
stContent = gstr.Replace(stContent, "``", "")
stContent = removeEndWrap(stContent)
buffer.Reset()
buffer.WriteString(stContent)
return buffer.String()
}
// genStructFieldForModel gens and returns the attribute definition for specified field.
func (l *gCurd) genStructFieldDefinition(in *genmodel.CurdPreviewParam, field *genmodel.GenCodesColumnListModel, inputType int, isMaster bool) []string {
var (
tagKey = "`"
result = []string{" #" + field.GoName}
descriptionTag = gstr.Replace(formatComment(field.Dc), `"`, `\"`)
)
addResult := func() []string {
isTreeManagedField := in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn
isTreeManagedField = isTreeManagedField || (in.Options.Step.IsTreeTable && in.Options.Tree.HasTreePath && (field.Name == in.Options.Tree.LevelColumn || field.Name == in.Options.Tree.TreeColumn))
if inputType == InputTypeEditParamFields && !IsIndexPK(field.Index) && !isTreeManagedField && !strings.HasPrefix(field.GoType, "*") {
result = append(result, " #*"+field.GoType)
} else {
result = append(result, " #"+field.GoType)
}
result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, field.TsName))
result = append(result, " #"+fmt.Sprintf(`dc:"%s"`+tagKey, descriptionTag))
return result
}
isUpdateOrInsertInputFields := func() bool {
if !isMaster {
return field.IsEdit
}
if field.IsEdit {
return true
}
// 树表内部维护字段
if in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn {
return true
}
if in.Options.Step.IsTreeTable && in.Options.Tree.HasTreePath && (field.Name == in.Options.Tree.LevelColumn || field.Name == in.Options.Tree.TreeColumn) {
return true
}
// 修改人
if inputType == InputTypeUpdateFields && field.GoName == "UpdatedBy" {
return true
}
if inputType == InputTypeUpdateFields && field.GoName == "UpdatedAt" {
return true
}
// 创建人
if inputType == InputTypeInsertFields && field.GoName == "CreatedBy" {
return true
}
if inputType == InputTypeInsertFields && field.GoName == "CreatedAt" {
return true
}
if inputType == InputTypeInsertFields && field.GoName == "CreatedDept" {
return true
}
if inputType == InputTypeInsertFields && field.GoName == "TenantId" {
return true
}
return false
}
isQuery := false
switch inputType {
case InputTypeListInp:
if in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn {
isQuery = true
field.QueryWhere = WMEq
}
if !field.IsQuery && !isQuery {
return nil
}
if field.QueryWhere == WMBetween {
result = append(result, " #[]"+field.GoType)
} else {
// 查询用户摘要时,固定接收字符串类型
if field.IsQuery && in.Options.Step.HasQueryMemberSummary && IsMemberSummaryField(field.Name) {
result = append(result, " #string")
} else {
result = append(result, " #"+field.GoType)
}
}
result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, field.TsName))
result = append(result, " #"+fmt.Sprintf(`dc:"%s"`+tagKey, descriptionTag))
case InputTypeListModel:
// 主表的主键
if IsIndexPK(field.Index) && isMaster {
addResult()
// 树表的pid字段
} else if in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn {
addResult()
} else if field.IsList {
addResult()
} else {
return nil
}
case InputTypeExportModel:
if !field.IsExport {
return nil
}
addResult()
case InputTypeEditInpValidator:
if !field.IsEdit {
return nil
}
if !field.Required && (field.FormRole == "none" || field.FormRole == "") {
return nil
}
rule := "// 验证" + field.Dc + "\n"
if field.Required && (field.FormRole == FRNone || field.FormRole == "") {
field.FormRole = "required"
}
if err, s := makeValidatorFunc(field); err != nil {
return nil
} else {
rule += s
}
result = []string{rule}
case InputTypeUpdateFields:
if !isUpdateOrInsertInputFields() {
return nil
}
addResult()
case InputTypeInsertFields:
if !isUpdateOrInsertInputFields() {
return nil
}
addResult()
case InputTypeTreeOptionFields:
if IsIndexPK(field.Index) {
return addResult()
}
if field.Name == in.Options.Tree.PidColumn {
return addResult()
}
if in.Options.Tree.TitleColumn == field.Name {
return addResult()
}
return nil
case InputTypeEditParamFields:
return addResult()
default:
panic("inputType is invalid")
}
return result
}
func makeValidatorFunc(field *genmodel.GenCodesColumnListModel) (err error, rule string) {
if field.FormRole == "required" {
rule = fmt.Sprintf(EditInpValidatorGenerally, "required", field.GoName, field.Dc+"不能为空")
} else if field.FormRole == FRIp {
rule = fmt.Sprintf(EditInpValidatorGenerally, "ip", field.GoName, field.Dc+"必须为IPV4或IPV6")
} else if field.FormRole == FRPercentage {
rule = fmt.Sprintf(EditInpValidatorGenerally, "min:0|max:100", field.GoName, field.Dc+"必须0-100之间")
} else if field.FormRole == FRTel {
rule = fmt.Sprintf(EditInpValidatorGenerally, "telephone", field.GoName, field.Dc+"不是座机号码")
} else if field.FormRole == FRPhone {
rule = fmt.Sprintf(EditInpValidatorGenerally, "phone", field.GoName, field.Dc+"不是手机号码")
} else if field.FormRole == FRQq {
rule = fmt.Sprintf(EditInpValidatorGenerally, "qq", field.GoName, field.Dc+"不是QQ号码")
} else if field.FormRole == FREmail {
rule = fmt.Sprintf(EditInpValidatorGenerally, "email", field.GoName, field.Dc+"不是邮箱地址")
} else if field.FormRole == FRIdCard {
rule = fmt.Sprintf(EditInpValidatorGenerally, "resident-id", field.GoName, field.Dc+"不是身份证号码")
} else if field.FormRole == FRNum {
rule = fmt.Sprintf(EditInpValidatorGenerally, "min:1", field.GoName, field.Dc+"必须大于0")
} else if field.FormRole == FRBankCard {
rule = fmt.Sprintf(EditInpValidatorGenerally, "bank-card", field.GoName, field.Dc+"不是银行卡号")
} else if field.FormRole == FRWeibo {
rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:^[0-9a-zA-Z\\u4e00-\\u9fa5_-]*$", field.GoName, field.Dc+"不是微博号")
} else if field.FormRole == FRUserName {
rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:^[0-9a-zA-Z]{6,16}$", field.GoName, field.Dc+"必须为6-16位由字母和数字组成")
} else if field.FormRole == FRAccount {
rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:^[\\w_\\d]{6,16}$", field.GoName, field.Dc+"必须为6-16位由字母、数字或下划线组成")
} else if field.FormRole == FRPassword {
rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,18}$", field.GoName, field.Dc+"必须包含6-18为字母和数字")
} else if field.FormRole == FRAmount {
rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:(^[0-9]{1,10}$)|(^[0-9]{1,10}[\\\\.]{1}[0-9]{1,2}$)", field.GoName, field.Dc+"最多允许输入10位整数及2位小数")
} else {
err = gerror.New("not support")
}
// 生成验证字典
if field.DictType != "" {
var (
ctx = context.Background()
valueType gdb.Value
resultValue gdb.Result
)
valueType, err = dao.SysDictType.Ctx(ctx).Fields(dao.SysDictType.Columns().DictType).Where(dao.SysDictType.Columns().DictId, field.DictType).Value()
if err != nil {
return
}
resultValue, err = dao.SysDictData.Ctx(context.Background()).Fields(dao.SysDictData.Columns().DictValue).Where(dao.SysDictData.Columns().DictType, valueType.String()).All()
if err != nil {
return
}
if resultValue.Len() > 0 {
names := make([]string, 0)
for _, item := range resultValue {
names = append(names, item["value"].String())
}
dictRule := "in:" + strings.Join(names, ",")
rule += fmt.Sprintf(EditInpValidatorGenerally, dictRule, field.GoName, field.Dc+"值不正确")
err = nil
}
}
return
}
@@ -0,0 +1,357 @@
// package genview
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genview
import (
"bytes"
"context"
"fmt"
genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts"
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/text/gstr"
)
const (
LogicWhereComments = "\n\t// 查询%s\n"
LogicWhereNoSupport = "\t// TODO 暂不支持生成[ %s ]查询方式,请自行补充此处代码!"
LogicEditUpdate = "\t\tdata := do.%s{}\n\t\tgconv.Struct(in, &data)\n\t\tdata.%s = nil\n\t\tif _, err = s.Model(ctx%s).TX(tx).\n\t\t\tFields(%sin.%sUpdateFields{}).\n\t\t\tWherePri(in.%s).Data(data).Update(); err != nil {\n\t\t\terr = gerror.Wrap(err, \"修改%s失败,请稍后重试!\")\n\t\t}\n\t\treturn"
LogicEditInsert = "\t\tdata := do.%s{}\n\t\tgconv.Struct(in, &data)\n\t\tdata.%s = nil\n\t\tif _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).TX(tx).\n\t\t\tFields(%sin.%sInsertFields{}).\n\t\t\tData(data).Insert(); err != nil {\n\t\t\terr = gerror.Wrap(err, \"新增%s失败,请稍后重试!\")\n\t\t}"
LogicEditUnique = "\t// 验证'%s'唯一\n\tif err = xgorm.IsUnique(ctx, &dao.%s, g.Map{dao.%s.Columns().%s: in.%s}, \"%s已存在\", in.%s); err != nil {\n\t\treturn\n\t}\n"
LogicSwitchUpdate = "%s"
LogicStatusUpdate = "do.%s{\n\t\tStatus: in.Status,\n%s}"
LogicDeletedUpdate = "do.%s{\n%s}"
)
func (l *gCurd) logicTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) {
g.Log().Infof(ctx, "logicTplData in.Options.Join: %+v", in.Options.Join)
data = make(g.Map)
data["listWhere"] = l.genLogicListWhere(ctx, in)
data["listJoin"] = l.genLogicListJoin(ctx, in)
data["listFields"] = l.genLogicListFields(ctx, in)
data["listOrder"] = l.genLogicListOrder(ctx, in)
data["edit"] = l.genLogicEdit(ctx, in)
data["switchFields"] = l.genLogicSwitchFields(ctx, in)
data["switchUpdate"] = l.genLogicSwitchUpdate(ctx, in)
data["statusUpdate"] = l.genLogicStatusUpdate(ctx, in)
data["deletedUpdate"] = l.genLogicDeletedUpdate(ctx, in)
return
}
func (l *gCurd) genLogicDeletedUpdate(ctx context.Context, in *genmodel.CurdPreviewParam) string {
hasDeletedAt := false
var update string
for _, field := range in.MasterFields {
switch field.GoName {
case "DeletedBy":
update += "\t\tDeletedBy: contexts.GetUserId(ctx),\n"
case "DeletedAt":
update += "\t\tDeletedAt: gtime.Now(),\n"
hasDeletedAt = true
}
}
if !hasDeletedAt {
return ""
}
update += "\t"
return fmt.Sprintf(LogicDeletedUpdate, in.In.DaoName, update)
}
func (l *gCurd) genLogicStatusUpdate(ctx context.Context, in *genmodel.CurdPreviewParam) string {
var update string
for _, field := range in.MasterFields {
switch field.GoName {
case "UpdatedBy":
update += "\t\tUpdatedBy: contexts.GetUserId(ctx),\n"
case "UpdatedAt":
update += "\t\tUpdatedAt: gtime.Now(),\n"
}
}
update += "\t"
return fmt.Sprintf(LogicStatusUpdate, in.In.DaoName, update)
}
func (l *gCurd) genLogicSwitchUpdate(ctx context.Context, in *genmodel.CurdPreviewParam) string {
buffer := bytes.NewBuffer(nil)
buffer.WriteString("\tvar data do." + in.In.DaoName + "\n")
buffer.WriteString("\tswitch in.Key {\n")
for _, field := range in.MasterFields {
if field.FormMode != FMSwitch {
continue
}
buffer.WriteString("\tcase \"" + field.Name + "\":\n")
buffer.WriteString("\t\tdata." + field.GoName + " = " + switchValueExpression(field.GoType) + "\n")
}
buffer.WriteString("\tdefault:\n")
buffer.WriteString("\t\terr = gerror.New(\"开关键名不在白名单\")\n")
buffer.WriteString("\t\treturn\n")
buffer.WriteString("\t}\n")
for _, field := range in.MasterFields {
switch field.GoName {
case "UpdatedBy":
buffer.WriteString("\tdata.UpdatedBy = contexts.GetUserId(ctx)\n")
case "UpdatedAt":
buffer.WriteString("\tdata.UpdatedAt = gtime.Now()\n")
}
}
return fmt.Sprintf(LogicSwitchUpdate, buffer.String())
}
func switchValueExpression(goType string) string {
switch goType {
case GoTypeBool:
return "gconv.Bool(in.Value)"
case GoTypeInt:
return "gconv.Int(in.Value)"
case GoTypeUint:
return "gconv.Uint(in.Value)"
case GoTypeUint64:
return "gconv.Uint64(in.Value)"
case GoTypeInt64:
return "gconv.Int64(in.Value)"
case GoTypeFloat32:
return "gconv.Float32(in.Value)"
case GoTypeFloat64:
return "gconv.Float64(in.Value)"
case GoTypeString:
return "gconv.String(in.Value)"
default:
return "gconv.Int(in.Value)"
}
}
func (l *gCurd) genLogicSwitchFields(ctx context.Context, in *genmodel.CurdPreviewParam) string {
buffer := bytes.NewBuffer(nil)
if in.Options.Step.HasSwitch {
for _, field := range in.MasterFields {
if field.FormMode == FMSwitch {
buffer.WriteString("\t\tdao." + in.In.DaoName + ".Columns()." + field.GoName + ",\n")
}
}
}
return buffer.String()
}
func (l *gCurd) genLogicEdit(ctx context.Context, in *genmodel.CurdPreviewParam) g.Map {
var (
data = make(g.Map)
updateBuffer = bytes.NewBuffer(nil)
insertBuffer = bytes.NewBuffer(nil)
uniqueBuffer = bytes.NewBuffer(nil)
)
for _, field := range in.MasterFields {
if field.GoName == "TenantId" {
insertBuffer.WriteString("\t\ttenantId := contexts.GetTenantId(ctx)\n\t\tin.TenantId = &tenantId\n")
}
if field.GoName == "CreatedDept" {
insertBuffer.WriteString("\t\tin.CreatedDept = contexts.GetDeptId(ctx)\n")
}
if field.GoName == "CreatedAt" {
insertBuffer.WriteString("\t\tin.CreatedAt = gtime.Now()\n")
}
if field.GoName == "CreatedBy" {
insertBuffer.WriteString("\tin.CreatedBy = contexts.GetUserId(ctx)\n")
}
if field.GoName == "UpdatedBy" {
updateBuffer.WriteString("\t\tin.UpdatedBy = contexts.GetUserId(ctx)\n")
}
if field.GoName == "UpdatedAt" {
updateBuffer.WriteString("\t\tin.UpdatedAt = gtime.Now()\n")
}
if field.Unique {
uniqueBuffer.WriteString(fmt.Sprintf(LogicEditUnique, field.GoName, in.In.DaoName, in.In.DaoName, field.GoName, field.GoName, field.Dc, in.Pk.GoName))
}
}
notFilterAuth := ""
if gstr.InArray(in.Options.ColumnOps, "notFilterAuth") {
notFilterAuth = ", &handler.Option{FilterAuth: false}"
}
updateBuffer.WriteString(fmt.Sprintf(LogicEditUpdate, in.In.DaoName, in.Pk.GoName, notFilterAuth, in.Options.TemplateGroup, in.In.VarName, in.Pk.GoName, in.In.TableComment))
insertBuffer.WriteString(fmt.Sprintf(LogicEditInsert, in.In.DaoName, in.Pk.GoName, in.Options.TemplateGroup, in.In.VarName, in.In.TableComment))
data["update"] = updateBuffer.String()
data["insert"] = insertBuffer.String()
data["unique"] = uniqueBuffer.String()
return data
}
func (l *gCurd) genLogicListOrder(ctx context.Context, in *genmodel.CurdPreviewParam) string {
statement := ""
if hasEffectiveJoins(in.Options.Join) {
statement = "dao." + in.In.DaoName + ".Table() + \".\" +"
}
buffer := bytes.NewBuffer(nil)
if in.Options.Step.HasMaxSort {
buffer.WriteString("OrderAsc(" + statement + "dao." + in.In.DaoName + ".Columns().Sort).")
}
buffer.WriteString("OrderDesc(" + statement + "dao." + in.In.DaoName + ".Columns()." + in.Pk.GoName + ")")
return buffer.String()
}
func (l *gCurd) genLogicListJoin(ctx context.Context, in *genmodel.CurdPreviewParam) (link string) {
connector := `"="`
if hasEffectiveJoins(in.Options.Join) {
linkBuffer := bytes.NewBuffer(nil)
for _, join := range in.Options.Join {
if isEffectiveJoin(join) {
linkBuffer.WriteString("\tmod = mod." + genconsts.GenCodesJoinLinkMap[join.LinkMode] + "OnFields(dao." + join.DaoName + ".Table(), dao." + in.In.DaoName + ".Columns()." + gstr.CaseCamel(join.MasterField) + "," + connector + ", dao." + join.DaoName + ".Columns()." + gstr.CaseCamel(join.Field) + ")\n")
}
}
link = linkBuffer.String()
}
return
}
func (l *gCurd) genLogicListFields(ctx context.Context, in *genmodel.CurdPreviewParam) (fields string) {
selectBuffer := bytes.NewBuffer(nil)
if hasEffectiveJoins(in.Options.Join) {
selectBuffer.WriteString("mod = mod.FieldsPrefix(dao." + in.In.DaoName + ".Table(), " + in.Options.TemplateGroup + "in." + in.In.VarName + "ListModel{})\n")
for _, join := range in.Options.Join {
if isEffectiveJoin(join) {
selectBuffer.WriteString("mod = mod.Fields(xgorm.JoinFields(ctx, " + in.Options.TemplateGroup + "in." + in.In.VarName + "ListModel{}, &dao." + join.DaoName + ", \"" + join.Alias + "\"))\n")
}
}
fields = selectBuffer.String()
} else {
fields = fmt.Sprintf("mod = mod.Fields(%sin.%sListModel{})", in.Options.TemplateGroup, in.In.VarName)
}
return
}
func (l *gCurd) genLogicListWhere(ctx context.Context, in *genmodel.CurdPreviewParam) string {
buffer := bytes.NewBuffer(nil)
// 主表
l.genLogicListWhereEach(buffer, in, in.MasterFields, in.In.DaoName, "")
// 关联表
g.Log().Infof(ctx, "genLogicListWhere in.Options.Join: %+v", in.Options.Join)
if hasEffectiveJoins(in.Options.Join) {
for _, v := range in.Options.Join {
if isEffectiveJoin(v) {
l.genLogicListWhereEach(buffer, in, v.Columns, v.DaoName, v.Alias)
}
}
}
return buffer.String()
}
func (l *gCurd) genLogicListWhereEach(buffer *bytes.Buffer, in *genmodel.CurdPreviewParam, fields []*genmodel.GenCodesColumnListModel, daoName string, alias string) {
isLink := false
if alias != "" {
alias = `"` + alias + `."+`
isLink = true
}
tablePrefix := ""
wherePrefix := "Where"
if isLink {
wherePrefix = "WherePrefix"
tablePrefix = "dao." + daoName + ".Table(), "
}
for _, field := range fields {
isQuery := false
// 树表查询上级
if in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn {
isQuery = true
field.QueryWhere = WMEq
}
if (!field.IsQuery && !isQuery) || field.QueryWhere == "" {
continue
}
buffer.WriteString(fmt.Sprintf(LogicWhereComments, field.Dc))
var (
linkMode string
whereTag string
columnName string
)
// 查询用户摘要
if field.IsQuery && in.Options.Step.HasQueryMemberSummary && IsMemberSummaryField(field.Name) {
servicePackName := "service"
if in.Options.Step.IsAddon {
servicePackName = "isc"
}
buffer.WriteString(fmt.Sprintf("if in.%v != \"\" {\n\t\t\t\tids, err := %v.AdminMember().GetIdsByKeyword(ctx, in.%v)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, 0, err\n\t\t\t\t}\n\t\t\t\tmod = mod.WhereIn(dao.%v.Columns().%v, ids)\n\t\t\t}\n", field.GoName, servicePackName, field.GoName, in.In.DaoName, field.GoName))
continue
}
if IsNumberType(field.GoType) {
linkMode = `in.` + field.GoName + ` > 0`
} else if field.GoType == GoTypeGTime {
linkMode = `in.` + field.GoName + ` != nil`
} else if field.GoType == GoTypeJson {
linkMode = `!in.` + field.GoName + `.IsNil()`
} else {
linkMode = `in.` + field.GoName + ` != ""`
}
if field.QueryWhere == WMBetween || field.QueryWhere == WMNotBetween {
linkMode = `len(in.` + field.GoName + `) == 2`
}
// 如果是关联表重新转换字段
columnName = field.GoName
if isLink {
columnName = gstr.CaseCamel(field.Name)
}
if field.FormMode == FMDateRange {
tmp := "\tif " + linkMode + " {\n\t\tin." + field.GoName + "[1] = in." + field.GoName + "[1].EndOfDay()\n\t}\n"
buffer.WriteString(tmp)
}
switch field.QueryWhere {
case WMEq:
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
case WMNeq:
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "Not(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
case WMGt:
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "GT(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
case WMGte:
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "GTE(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
case WMLt:
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "LT(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
case WMLte:
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "LTE(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
case WMIn:
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "In(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
case WMNotIn:
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "NotIn(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
case WMBetween:
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "Between(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + "[0], in." + field.GoName + "[1])\n\t}"
case WMNotBetween:
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "NotBetween(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + "[0], in." + field.GoName + "[1])\n\t}"
case WMLike:
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "Like(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
case WMLikeAll:
val := `"%"+in.` + field.GoName + `+"%"`
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "Like(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", " + val + ")\n\t}"
case WMNotLike:
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "NotLike(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
case WMJsonContains:
val := tablePrefix + `"JSON_CONTAINS("+dao.` + daoName + `.Columns().` + columnName + `+",?)", in.` + field.GoName
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "(" + val + ")\n\t}"
default:
buffer.WriteString(fmt.Sprintf(LogicWhereNoSupport, field.QueryWhere))
}
buffer.WriteString(whereTag + "\n")
}
}
@@ -0,0 +1,903 @@
// package genview
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genview
import (
"bytes"
"context"
"fmt"
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
"strconv"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
type StateItem struct {
Name string
DefaultValue interface{}
Dc string
}
func (l *gCurd) webModelTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) {
data = make(g.Map)
data["stateItems"] = l.genWebModelStateItems(ctx, in)
data["rules"] = l.genWebModelRules(ctx, in)
data["formSchema"] = l.genWebModelFormSchema(ctx, in)
if data["columns"], err = l.genWebModelColumns(ctx, in); err != nil {
return nil, err
}
if data["columnsInterface"], err = l.genWebModelColumnsInterface(ctx, in); err != nil {
return nil, err
}
data["viewSchema"] = l.genWebModelViewSchema(ctx, in)
data["editSchema"] = l.genWebModelEditSchema(ctx, in)
// 根据表单生成情况,按需导包
data["import"], data["const"] = l.genWebModelImport(ctx, in)
return
}
func (l *gCurd) genWebModelImport(ctx context.Context, in *genmodel.CurdPreviewParam) (string, string) {
importBuffer := bytes.NewBuffer(nil)
constBuffer := bytes.NewBuffer(nil)
importBuffer.WriteString(`import { h, ref } from 'vue';
import { Tag } from 'ant-design-vue';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getPopupContainer } from '@vben/utils';
import type { DescItem } from '#/components/description';
`)
// 导入基础组件
if len(in.Options.Step.ImportModel.NaiveUI) > 0 {
importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.NaiveUI) + " from 'ant-design-vue';\n")
}
importBuffer.WriteString("import { cloneDeep } from 'lodash-es';\n")
// 导入表单搜索
if in.Options.Step.HasSearchForm {
importBuffer.WriteString("import type { VbenFormSchema } from '@vben/common-ui';\n")
}
// 导入工具类
if len(in.Options.Step.ImportModel.UtilsIs) > 0 {
importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.UtilsIs) + " from '#/utils/is';\n")
}
if len(in.Options.Step.ImportModel.UtilsUrl) > 0 {
importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.UtilsUrl) + " from '#/utils/urlUtils';\n")
}
if len(in.Options.Step.ImportModel.UtilsDate) > 0 {
// importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.UtilsDate) + " from '@/utils/dateUtil';\n")
importBuffer.WriteString("import dayjs from 'dayjs';\n")
}
if in.Options.Step.HasRulesValidator {
importBuffer.WriteString("import { z } from '@vben/common-ui';\n")
}
if len(in.Options.Step.ImportModel.UtilsIndex) > 0 {
importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.UtilsIndex) + " from '#/utils';\n")
}
// 导入api
var importApiMethod []string
if in.Options.Step.HasSwitch {
importApiMethod = append(importApiMethod, "Switch")
}
if in.Options.Step.IsTreeTable {
importApiMethod = append(importApiMethod, "TreeOption")
}
if len(importApiMethod) > 0 {
importBuffer.WriteString("import " + ImportWebMethod(importApiMethod) + " from '" + in.Options.ImportWebApi + "';\n")
}
// 导入字典选项
if in.Options.DictOps.Has {
importBuffer.WriteString(`import { DictEnum } from '@vben/constants';
import { getDictOptions } from '#/utils/dict';`)
constBuffer.WriteString("\n")
}
if in.Options.Step.HasSwitch {
importBuffer.WriteString("import { usePermission } from '@/hooks/web/usePermission';\n")
constBuffer.WriteString("const { hasPermission } = usePermission();\n")
constBuffer.WriteString("const $message = window['$message'];\n")
}
return importBuffer.String(), constBuffer.String()
}
func (l *gCurd) genWebModelStateItems(ctx context.Context, in *genmodel.CurdPreviewParam) (items []*StateItem) {
for _, field := range in.MasterFields {
var value = webModelDefaultValue(field)
// 选项组件默认值调整
if gconv.Int(value) == 0 && IsSelectFormModel(field.FormMode) {
value = "null"
}
if field.Name == "status" {
value = 0
}
if field.FormMode == FMSwitch {
value = 0
}
if field.FormMode == FMInputDynamic {
value = "[]"
}
items = append(items, &StateItem{
Name: field.TsName,
DefaultValue: value,
Dc: field.Dc,
})
// 查询用户摘要
if field.IsList && in.Options.Step.HasHookMemberSummary && IsMemberSummaryField(field.Name) {
items = append(items, &StateItem{
Name: field.TsName + "Summa?: null | MemberSumma",
DefaultValue: "null",
Dc: field.Dc + "摘要信息",
})
}
}
return
}
func webModelDefaultValue(field *genmodel.GenCodesColumnListModel) interface{} {
value := field.DefaultValue
if value == nil {
return "null"
}
if gconv.String(value) == "NULL" {
return "null"
}
switch field.GoType {
case GoTypeString, GoTypeBytes, GoTypeDate, GoTypeDatetime, GoTypeTime, GoTypeGTime:
return strconv.Quote(gconv.String(value))
default:
if value == "" {
return "null"
}
return value
}
}
func (l *gCurd) genWebModelDictOptions(ctx context.Context, in *genmodel.CurdPreviewParam) error {
type DictType struct {
DictType string `json:"dictType"`
DictName string `json:"dictName"`
}
var (
dictTypeIds []string
dictTypeList []*DictType
builtinDictTypeIds []int64
builtinDictTypeList []*DictType
)
for _, field := range in.MasterFields {
if field.DictType != "" {
dictTypeIds = append(dictTypeIds, field.DictType)
}
// if field.DictType < 0 {
// builtinDictTypeIds = append(builtinDictTypeIds, field.DictType)
// }
}
dictTypeIds = utility.UniqueSlice(dictTypeIds)
builtinDictTypeIds = utility.UniqueSlice(builtinDictTypeIds)
if len(dictTypeIds) == 0 && len(builtinDictTypeIds) == 0 {
return nil
}
if len(dictTypeIds) > 0 {
err := g.Model("sys_dict_type").Ctx(ctx).
Fields("dict_type", "dict_name").
WhereIn("dict_type", dictTypeIds).
Scan(&dictTypeList)
if err != nil {
return err
}
}
// if len(builtinDictTypeIds) > 0 {
// for _, id := range builtinDictTypeIds {
// typ, err := dict.GetTypeById(ctx, id)
// if err != nil && !errors.Is(err, dict.NotExistKeyError) {
// return err
// }
// if len(typ) > 0 {
// row := new(DictType)
// row.Id = id
// row.Type = typ
// builtinDictTypeList = append(builtinDictTypeList, row)
// }
// }
// }
if len(dictTypeList) == 0 && len(builtinDictTypeList) == 0 {
return nil
}
if len(builtinDictTypeList) > 0 {
dictTypeList = append(dictTypeList, builtinDictTypeList...)
}
in.Options.DictOps.Has = true
for _, v := range dictTypeList {
// 字段映射字典
for _, field := range in.MasterFields {
if field.DictType != "" && v.DictType == field.DictType {
in.Options.DictMap[field.TsName] = v.DictType
in.Options.DictOps.Schemas = append(in.Options.DictOps.Schemas, &genmodel.OptionsSchemasField{
Field: field.TsName,
Type: v.DictType,
})
}
}
in.Options.DictOps.Types = append(in.Options.DictOps.Types, v.DictName)
}
return nil
}
func (l *gCurd) genWebModelRules(ctx context.Context, in *genmodel.CurdPreviewParam) string {
// buffer := bytes.NewBuffer(nil)
// buffer.WriteString("export const rules = {\n")
// for _, field := range in.MasterFields {
// if !field.IsEdit || (!field.Required && (field.FormRole == "" || field.FormRole == FRNone)) {
// continue
// }
// in.Options.Step.HasRules = true
// if field.FormRole == "" || field.FormRole == FRNone || field.FormRole == "required" {
// buffer.WriteString(fmt.Sprintf(" %s: {\n required: %v,\n trigger: ['blur', 'input'],\n type: '%s',\n message: '请输入%s',\n },\n", field.TsName, field.Required, field.TsType, field.Dc))
// } else {
// in.Options.Step.HasRulesValidator = true
// buffer.WriteString(fmt.Sprintf(" %s: {\n required: %v,\n trigger: ['blur', 'input'],\n type: '%s',\n validator: validate.%v,\n },\n", field.TsName, field.Required, field.TsType, field.FormRole))
// }
// }
// buffer.WriteString("};\n")
// return buffer.String()
return ""
}
func (l *gCurd) genWebModelFormSchema(ctx context.Context, in *genmodel.CurdPreviewParam) string {
buffer := bytes.NewBuffer(nil)
buffer.WriteString("export const querySchema: VbenFormSchema[] = [\n")
// 主表
l.genWebModelFormSchemaEach(buffer, in.MasterFields, in, false)
// 关联表
if len(in.Options.Join) > 0 {
for _, v := range in.Options.Join {
if !isEffectiveJoin(v) {
continue
}
l.genWebModelFormSchemaEach(buffer, v.Columns, in, false)
}
}
buffer.WriteString("];\n")
return buffer.String()
}
// genWebDictOption 生产字典选项
func (l *gCurd) genWebDictOption(typ any) string {
return fmt.Sprintf(`getDictOptions('%v')`, typ)
}
func (l *gCurd) genWebModelFormSchemaEach(buffer *bytes.Buffer, fields []*genmodel.GenCodesColumnListModel, in *genmodel.CurdPreviewParam, isEdit bool) {
for _, field := range fields {
if isEdit {
if !field.IsEdit && field.Name != in.Pk.Name {
continue
}
if field.Name == in.Pk.Name {
buffer.WriteString(fmt.Sprintf(` {
fieldName: '%s',
component: '%s',
label: '%s',
dependencies: { show: () => false, triggerFields: [''], },
componentProps: {
placeholder: '',
onUpdateValue: (e: any) => {
console.log(e);
},
},
formItemClass: 'col-span-%d',
},`, field.TsName, "Input", field.Dc, field.FormGridSpan))
continue
}
} else {
if !field.IsQuery {
continue
}
}
rules := "null"
if in.Options.Step.IsTreeTable && isEdit && field.Name == in.Options.Tree.PidColumn {
field.FormMode = FMPidTreeSelect
}
if field.IsEdit {
if field.Required {
if field.FormMode == FMInputNumber {
rules = fmt.Sprintf("z.number({required_error: '请输入%s', invalid_type_error: '无效数字'})", field.Dc)
in.Options.Step.HasRulesValidator = true
} else if field.FormMode == FMSelect || field.FormMode == FMSelectMultiple || field.FormMode == FMCheckbox || field.FormMode == FMDate || field.FormMode == FMDateRange || field.FormMode == FMTime || field.FormMode == FMTimeRange {
rules = "'selectRequired'"
} else {
rules = "'required'"
}
}
}
if field.FormRole != "" {
in.Options.Step.HasRulesValidator = true
if field.FormRole == FRNum {
rules = fmt.Sprintf("z.number({required_error: '请输入%s', invalid_type_error: '无效数字'} )", field.Dc)
}
if field.FormRole == FRIp {
rules = "z.string().ip({message: '无效IP格式'})"
}
if field.FormRole == FRPhone {
rules = "z.string().regex(/^1[3-9]\\d{9}$/, '无效手机号格式')"
}
if field.FormRole == FRTel {
rules = "z.string().regex(/^(?:\\+?\\d{1,3}[-.\\s]?)?(?:$?\\d{1,4}$?[-.\\s]?)?\\d{1,4}[-.\\s]?\\d{1,4}[-.\\s]?\\d{1,9}$/, '无效手机号格式')"
}
if field.FormRole == FRPercentage {
rules = fmt.Sprintf("z.number({required_error: '请输入%s', invalid_type_error: '无效百分比'}).min(0).max(100)", field.Dc)
}
if field.FormRole == FRQq {
rules = "z.string().regex(/^[1-9]\\d{4,}$/, '无效QQ号格式')"
}
if field.FormRole == FRAmount {
rules = "z.number({required_error: '必填', invalid_type_error: '无效金额'} ).min(0)"
}
if field.FormRole == FRBankCard {
rules = "z.string().regex(/^[1-9]\\d{4,}$/, '无效银行卡号格式')"
}
if field.FormRole == FRWeibo {
rules = "z.string().regex(/^[1-9]\\d{4,}$/, '无效微博号格式')"
}
if field.FormRole == FRUserName {
rules = "z.string().regex(/^[a-zA-Z0-9_]+$/, '无效用户名格式')"
}
if field.FormRole == FRPassword {
rules = "z.string().regex(/^[a-zA-Z0-9_]+$/, '无效密码格式')"
}
if field.FormRole == FRAccount {
rules = "z.string().regex(/^[a-zA-Z0-9_]+$/, '无效账号格式')"
}
if field.FormRole == FRIdCard {
rules = "z.string().regex(/^[1-9]\\d{4,}$/, '无效身份证号格式')"
}
if field.FormRole == FREmail {
rules = "z.string().email('无效邮箱格式')"
}
}
in.Options.Step.HasSearchForm = true
// 查询用户摘要
if field.IsQuery && in.Options.Step.HasQueryMemberSummary && IsMemberSummaryField(field.Name) {
buffer.WriteString(fmt.Sprintf(` {
fieldName: '%s',
component: '%s',
label: '%s',
componentProps: {
placeholder: '请输入ID|用户名|姓名|手机号',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules:%v,
formItemClass: 'col-span-%d',
},`, field.TsName, "Input", field.Dc, rules, field.FormGridSpan))
continue
}
var (
defaultComponent = fmt.Sprintf(` {
fieldName: '%s',
component: '%s',
label: '%s',
componentProps: {
placeholder: '请输入%s',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules:%v,
formItemClass: 'col-span-%d',
},`, field.TsName, "Input", field.Dc, field.Dc, rules, field.FormGridSpan)
component string
)
// 这里根据编辑表单组件来进行推断,如果没有则使用默认input,这可能会导致和查询条件所需参数不符的情况
switch field.FormMode {
case FMInput, FMInputTextarea, FMInputEditor:
component = defaultComponent
case FMInputNumber:
component = fmt.Sprintf(` {
fieldName: '%s',
component: '%s',
label: '%s',
componentProps: {
placeholder: '请输入%s',
onUpdateValue: (e: any) => {
console.log(e);
},
}, rules:%v,
formItemClass: 'col-span-%d',
},`, field.TsName, "InputNumber", field.Dc, field.Dc, rules, field.FormGridSpan)
case FMDate:
component = fmt.Sprintf(` {
fieldName: '%s',
component: '%s',
label: '%s',
componentProps: {
type: '%s',
clearable: true,
shortcuts: %s,
onUpdateValue: (e: any) => {
console.log(e);
},
}, rules:%v,
formItemClass: 'col-span-%d',
},`, field.TsName, "DatePicker", field.Dc, "date", "'FMDate'", rules, field.FormGridSpan)
in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defShortcuts")
case FMDateRange:
component = fmt.Sprintf(` {
fieldName: '%s',
component: '%s',
label: '%s',
componentProps: {
type: '%s',
clearable: true,
valueFormat: %s,
onUpdateValue: (e: any) => {
console.log(e);
},
}, rules:%v,
formItemClass: 'col-span-%d',
},`, field.TsName, "RangePicker", field.Dc, "datetimerange", "'FMDateRange'", rules, field.FormGridSpan)
in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defRangeShortcuts")
// in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defRangeShortcuts")
case FMTime:
component = fmt.Sprintf(` {
fieldName: '%s',
component: '%s',
label: '%s',
componentProps: {
type: '%s',
clearable: true,
showTime: true,
shortcuts: %s,
onUpdateValue: (e: any) => {
console.log(e);
},
}, rules:%v,
formItemClass: 'col-span-%d',
},`, field.TsName, "DatePicker", field.Dc, "datetime", "'FMTime'", rules, field.FormGridSpan)
in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defShortcuts")
case FMTimeRange:
component = fmt.Sprintf(` {
fieldName: '%s',
component: '%s',
label: '%s',
componentProps: {
type: '%s',
clearable: true,
shortcuts: %s,
showTime: true,
onUpdateValue: (e: any) => {
console.log(e);
},
}, rules:%v,
formItemClass: 'col-span-%d',
},`, field.TsName, "RangePicker", field.Dc, "datetimerange", "'FMTimeRange'", rules, field.FormGridSpan)
in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defRangeShortcuts")
case FMSwitch:
fallthrough
case FMRadio:
component = fmt.Sprintf(` {
fieldName: '%s',
component: '%s',
label: '%s',
componentProps: {
options: %v
},
rules:%v,
formItemClass: 'col-span-%d',
},
`, field.TsName, "RadioGroup", field.Dc, l.genWebDictOption(in.Options.DictMap[field.TsName]), rules, field.FormGridSpan)
case FMCheckbox:
component = fmt.Sprintf(` {
fieldName: '%s',
component: '%s',
label: '%s',
componentProps: {
placeholder: '请选择%s',
options: %v
},
rules:%v,
formItemClass: 'col-span-%d',
},
`, field.TsName, "CheckboxGroup", field.Dc, field.Dc,
l.genWebDictOption(in.Options.DictMap[field.TsName]), rules, field.FormGridSpan)
case FMSelect:
component = fmt.Sprintf(` {
fieldName: '%s',
component: '%s',
label: '%s',
defaultValue: null,
componentProps: {
placeholder: '请选择%s',
options: %v,
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules:%v,
formItemClass: 'col-span-%d',
},
`, field.TsName, "Select", field.Dc, field.Dc, l.genWebDictOption(in.Options.DictMap[field.TsName]), rules, field.FormGridSpan)
case FMSelectMultiple:
component = fmt.Sprintf(` {
fieldName: '%s',
component: '%s',
label: '%s',
defaultValue: null,
componentProps: {
mode: 'multiple',
placeholder: '请选择%s',
options: %v,
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules:%v,
formItemClass: 'col-span-%d',
},
`, field.TsName, "Select", field.Dc, field.Dc, l.genWebDictOption(in.Options.DictMap[field.TsName]), rules, field.FormGridSpan)
case FMPidTreeSelect:
component = fmt.Sprintf(` {
fieldName: '%s',
component: '%s',
label: '%s',
defaultValue: 0,
componentProps: {
allowClear: true,
fieldNames: { label: '%s', value: '%s' },
showSearch: true,
treeData: [],
treeDefaultExpandAll: true,
treeLine: { showLeafIcon: false },
treeNodeFilterProp: '%s',
},
rules:%v,
formItemClass: 'col-span-%d',
},
`, field.TsName, "TreeSelect", field.Dc, in.Options.Tree.TitleField.TsName, in.Pk.TsName, in.Options.Tree.TitleField.TsName, rules, field.FormGridSpan)
default:
component = defaultComponent
}
buffer.WriteString(component)
}
}
func (l *gCurd) genWebModelColumns(ctx context.Context, in *genmodel.CurdPreviewParam) (string, error) {
buffer := bytes.NewBuffer(nil)
buffer.WriteString("export const columns: VxeGridProps['columns'] = [\n")
// 主表
if err := l.genWebModelColumnsEach(buffer, in, in.MasterFields); err != nil {
return "", err
}
// 关联表
if len(in.Options.Join) > 0 {
for _, v := range in.Options.Join {
if !isEffectiveJoin(v) {
continue
}
if err := l.genWebModelColumnsEach(buffer, in, v.Columns); err != nil {
return "", err
}
}
}
buffer.WriteString(" { title: '操作', width: 120, slots: { default: 'action' } },\n")
buffer.WriteString("];\n")
return buffer.String(), nil
}
func (l *gCurd) genWebModelColumnsInterface(ctx context.Context, in *genmodel.CurdPreviewParam) (string, error) {
buffer := bytes.NewBuffer(nil)
buffer.WriteString("export interface RowType {\n")
// 主表
for _, field := range in.MasterFields {
buffer.WriteString(fmt.Sprintf(" %s: %s;\n", field.TsName, field.TsType))
}
// 关联表
if len(in.Options.Join) > 0 {
for _, v := range in.Options.Join {
if !isEffectiveJoin(v) {
continue
}
for _, field := range v.Columns {
buffer.WriteString(fmt.Sprintf(" %s: %s;\n", field.TsName, field.TsType))
}
}
}
buffer.WriteString("};\n")
return buffer.String(), nil
}
func (l *gCurd) genWebModelViewSchema(ctx context.Context, in *genmodel.CurdPreviewParam) string {
buffer := bytes.NewBuffer(nil)
buffer.WriteString("export const viewSchema: DescItem[] = [\n")
// 主表
for _, field := range in.MasterFields {
l.genWebModelViewSchemaEach(buffer, []*genmodel.GenCodesColumnListModel{field}, in)
}
// 关联表
if len(in.Options.Join) > 0 {
for _, v := range in.Options.Join {
if !isEffectiveJoin(v) {
continue
}
for _, field := range v.Columns {
l.genWebModelViewSchemaEach(buffer, []*genmodel.GenCodesColumnListModel{field}, in)
}
}
}
buffer.WriteString("];\n")
return buffer.String()
}
func (l *gCurd) genWebModelEditSchema(ctx context.Context, in *genmodel.CurdPreviewParam) string {
buffer := bytes.NewBuffer(nil)
buffer.WriteString("export const editSchema: VbenFormSchema[] = [\n")
// 主表
for _, field := range in.MasterFields {
l.genWebModelFormSchemaEach(buffer, []*genmodel.GenCodesColumnListModel{field}, in, true)
}
// 关联表
if len(in.Options.Join) > 0 {
for _, v := range in.Options.Join {
if !isEffectiveJoin(v) {
continue
}
for _, field := range v.Columns {
l.genWebModelFormSchemaEach(buffer, []*genmodel.GenCodesColumnListModel{field}, in, true)
}
}
}
buffer.WriteString("];\n")
return buffer.String()
}
func (l *gCurd) genWebModelColumnsEach(buffer *bytes.Buffer, in *genmodel.CurdPreviewParam, fields []*genmodel.GenCodesColumnListModel) (err error) {
for _, field := range fields {
if !field.IsList {
continue
}
if field.Name == in.Pk.Name {
buffer.WriteString(fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n type: 'checkbox',\n },\n", field.Dc, field.TsName, field.Align, field.Width))
continue
}
var (
defaultComponent = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n },\n", field.Dc, field.TsName, field.Align, field.Width)
component string
)
if in.Options.Step.IsTreeTable && in.Options.Tree.TitleColumn == field.Name {
defaultComponent = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n treeNode: true,\n },\n", field.Dc, field.TsName, field.Align, field.Width)
}
// 查询用户摘要
if in.Options.Step.HasHookMemberSummary && IsMemberSummaryField(field.Name) {
buffer.WriteString(fmt.Sprintf(" {\n title: '%v',\n field: '%v',\n align: '%v',\n width: %v,\n slots: {\n default: ({ row }) => {\n return renderPopoverMemberSumma(row.%vSumma);\n },\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName))
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, []string{"renderPopoverMemberSumma", "type MemberSumma"}...)
continue
}
// 这里根据编辑表单组件来进行推断,如果没有则使用默认input,这可能会导致和查询条件所需参数不符的情况
switch field.FormMode {
case FMDate:
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return formatDateTime(row.%s);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName)
in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "formatToDate")
case FMRadio:
fallthrough
case FMSelect:
if g.IsEmpty(in.Options.DictMap[field.TsName]) {
g.Log().Infof(context.Background(), "server/internal/library/xgen/gen_curd_model.go genWebModelColumnsEach in.Options.DictMap:%+v", in.Options.DictMap)
err = gerror.Newf("设置单选下拉框选项时,必须选择字典类型,字段名称:%v", field.Name)
return
}
component = fmt.Sprintf(` {
title: '%s', field: '%s', align: '%v', width: %v,
slots: {
default: ({ row }) => {
return renderDict(row.%v, '%v');
}
},
},
`,
field.Dc, field.TsName, field.Align, field.Width, field.TsName, in.Options.DictMap[field.TsName])
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDict")
case FMSelectMultiple:
if g.IsEmpty(in.Options.DictMap[field.TsName]) {
err = gerror.Newf("设置多选下拉框选项时,必须选择字典类型,字段名称:%v", field.Name)
return
}
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n if (isNullObject(row.%s) || !isArray(row.%s)) {\n return ``;\n }\n return row.%s.map((tagKey) => {\n return renderOptionTag('%s', row.tagKey)\n });\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName, field.TsName, field.TsName, in.Options.DictMap[field.TsName])
in.Options.Step.ImportModel.NaiveUI = append(in.Options.Step.ImportModel.NaiveUI, "NTag")
in.Options.Step.ImportModel.UtilsIs = append(in.Options.Step.ImportModel.UtilsIs, "isNullObject")
case FMUploadImage:
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderImage(row.%v);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName)
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderImage")
case FMUploadImages:
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderImageGroup(row.%v);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName)
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderImageGroup")
case FMUploadFile:
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderFile(row.%v);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName)
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderFile")
case FMUploadFiles:
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderFileGroup(row.%v);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName)
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderFileGroup")
case FMSwitch:
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return h(%s, {\n value: row.%s === 1,\n checked: '开启',\n unchecked: '关闭',\n disabled: !hasPermission(['%s']),\n onUpdateValue: function (e) {\n console.log('onUpdateValue e:' + JSON.stringify(e));\n row.%s = e ? 1 : 0;\n Switch({ %s: row.%s, key: '%s', value: row.%s }).then((_res) => {\n $message.success('操作成功');\n });\n },\n });\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, "NSwitch", field.TsName, "/"+in.Options.ApiPrefix+"/switch", field.TsName, in.Pk.TsName, in.Pk.TsName, CamelCaseToUnderline(field.TsName), field.TsName)
in.Options.Step.ImportModel.NaiveUI = append(in.Options.Step.ImportModel.NaiveUI, "NSwitch")
case FMRate:
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return h(%s, {\n allowHalf: true,\n readonly: true,\n defaultValue: row.%s,\n });\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, "NRate", field.TsName)
in.Options.Step.ImportModel.NaiveUI = append(in.Options.Step.ImportModel.NaiveUI, "NRate")
default:
component = defaultComponent
}
buffer.WriteString(component)
}
return
}
func (l *gCurd) genWebModelViewSchemaEach(buffer *bytes.Buffer, fields []*genmodel.GenCodesColumnListModel, in *genmodel.CurdPreviewParam) {
for _, field := range fields {
if field.Name == in.Pk.Name {
tmp := fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
buffer.WriteString(tmp)
continue
}
// 查询用户摘要
if field.IsQuery && in.Options.Step.HasQueryMemberSummary && IsMemberSummaryField(field.Name) {
tmp := fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
buffer.WriteString(tmp)
continue
}
var (
defaultComponent = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
component string
)
// 这里根据编辑表单组件来进行推断,如果没有则使用默认input,这可能会导致和查询条件所需参数不符的情况
switch field.FormMode {
case FMInput, FMInputTextarea, FMInputEditor:
component = defaultComponent
case FMInputNumber:
component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
case FMDate:
component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
case FMDateRange:
component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
case FMTime:
component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
case FMTimeRange:
component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
case FMSwitch:
fallthrough
case FMRadio:
component = fmt.Sprintf(` {
field: '%s',
label: '%s',
render(row: any) {
return renderDict(row.%s, '%v');
},
},
`, field.TsName, field.Dc, field.TsName, in.Options.DictMap[field.TsName])
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDict")
case FMCheckbox:
component = fmt.Sprintf(` {
field: '%s',
label: '%s',
render(row: any) {
return renderDictTags(row.%s, '%v');
},
},
`, field.TsName, field.Dc, field.TsName, in.Options.DictMap[field.TsName])
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDictTags")
case FMSelect:
component = fmt.Sprintf(` {
field: '%s',
label: '%s',
render(row: any) {
return renderDict(row.%s, '%v');
},
},
`, field.TsName, field.Dc, field.TsName, in.Options.DictMap[field.TsName])
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDict")
case FMSelectMultiple:
component = fmt.Sprintf(` {
field: '%s',
label: '%s',
render(row: any) {
return renderDictTags(row.%s, '%v');
},
},
`, field.TsName, field.Dc, field.TsName, in.Options.DictMap[field.TsName])
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDictTags")
default:
component = defaultComponent
}
buffer.WriteString(component)
}
}
@@ -0,0 +1,342 @@
// package genview
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genview
import (
"bytes"
"context"
"fmt"
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/text/gstr"
)
func (l *gCurd) webEditTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) {
data = make(g.Map)
data["formItem"] = l.genWebEditFormItem(ctx, in)
data["script"] = l.genWebEditScript(ctx, in)
data["isEditModal"] = in.Options.Step.IsEditModal
return
}
func (l *gCurd) genWebEditFormItem(ctx context.Context, in *genmodel.CurdPreviewParam) string {
buffer := bytes.NewBuffer(nil)
for _, field := range in.MasterFields {
if !field.IsEdit {
continue
}
if IsIndexPK(field.Index) {
continue
}
var (
defaultComponent = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<Input placeholder="请输入%s" v-model:value="formValue.%s" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.Dc, field.TsName)
component string
)
if in.Options.Step.IsTreeTable && IsPidName(field.Name) {
field.FormMode = FMPidTreeSelect
}
switch field.FormMode {
case FMInput:
component = defaultComponent
case FMInputNumber:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<InputNumber class="w-full" placeholder="请输入%s" v-model:value="formValue.%s" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.Dc, field.TsName)
case FMInputTextarea:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<Textarea placeholder="请输入%s" v-model:value="formValue.%s" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.Dc, field.TsName)
case FMInputEditor:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<RichTextarea style="height: 450px" v-model="formValue.%s" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
case FMInputDynamic:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<Space direction="vertical" class="w-full">
<Space v-for="(item, index) in formValue.%s" :key="index">
<Input v-model:value="item.key" placeholder="键名" />
<Input v-model:value="item.value" placeholder="键值" />
</Space>
</Space>
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
case FMDate:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<DatePicker v-model:value="formValue.%s" class="w-full" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
case FMTime:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<DatePicker v-model:value="formValue.%s" show-time class="w-full" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
case FMRadio:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<RadioGroup
v-model:value="formValue.%s"
:options="getDictOptions('%s')"
option-type="button"
button-style="solid"
/>
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName, in.Options.DictMap[field.TsName])
case FMCheckbox:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<CheckboxGroup v-model:value="formValue.%s">
<Space>
<Checkbox
v-for="item in getDictOptions('%s')"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</Checkbox>
</Space>
</CheckboxGroup>
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName, in.Options.DictMap[field.TsName])
case FMSelect:
if in.Options.DictMap[field.TsName] != nil {
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<Select v-model:value="formValue.%s" :options="getDictOptions('%s')" placeholder="请选择%s" class="w-full" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName, in.Options.DictMap[field.TsName], field.Dc)
} else {
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<Select v-model:value="formValue.%s" placeholder="请选择%s" class="w-full" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName, field.Dc)
}
case FMSelectMultiple:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<Select v-model:value="formValue.%s" mode="multiple" :options="getDictOptions('%s')" placeholder="请选择%s" class="w-full" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName, in.Options.DictMap[field.TsName], field.Dc)
case FMUploadImage:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<ImageUpload :max-count="1" v-model:value="formValue.%s" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
case FMUploadImages:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<ImageUpload :max-count="10" v-model:value="formValue.%s" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
case FMUploadFile:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<FileUpload :max-count="1" v-model:value="formValue.%s" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
case FMUploadFiles:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<FileUpload :max-count="10" v-model:value="formValue.%s" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
case FMSwitch:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<Switch v-model:checked="formValue.%s" :checked-value="1" :un-checked-value="0" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
case FMRate:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<Rate allow-half v-model:value="formValue.%s" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
case FMCitySelector:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%s" name="%s">
<Cascader v-model:value="formValue.%s" placeholder="请选择%s" class="w-full" />
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName, field.Dc)
case FMPidTreeSelect:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%v" name="pid">
<TreeSelect
v-model:value="formValue.pid"
:tree-data="treeOption"
:field-names="{ label: '%v', value: '%v', children: 'children' }"
allow-clear
show-search
tree-default-expand-all
:tree-node-filter-prop="'%v'"
class="w-full"
/>
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, in.Options.Tree.TitleField.TsName, in.Pk.TsName, in.Options.Tree.TitleField.TsName)
case FMTreeSelect:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%v" name="%v">
<TreeSelect
placeholder="请选择%v"
v-model:value="formValue.%v"
:tree-data="[{ title: 'AA', value: 1, children: [{ title: 'BB', value: 2 }] }]"
allow-clear
show-search
tree-default-expand-all
class="w-full"
/>
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.Dc, field.TsName)
case FMCascader:
component = fmt.Sprintf(`<Col :span="%d">
<FormItem label="%v" name="%v">
<Cascader
placeholder="请选择%v"
v-model:value="formValue.%v"
:options="[{ label: 'AA', value: 1, children: [{ label: 'BB', value: 2 }] }]"
allow-clear
show-search
class="w-full"
/>
</FormItem>
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.Dc, field.TsName)
default:
component = defaultComponent
}
buffer.WriteString(component + "\n\n")
}
return buffer.String()
}
func (l *gCurd) genWebEditScript(ctx context.Context, in *genmodel.CurdPreviewParam) g.Map {
var (
data = make(g.Map)
importBuffer = bytes.NewBuffer(nil)
antdUI []string
)
importBuffer.WriteString(" import { ref, computed } from 'vue';\n")
// 导入字典
if in.Options.DictOps.Has {
importBuffer.WriteString(" import { getDictOptions } from '#/utils/dict';\n")
}
// 导入api
var importApiMethod = []string{"Edit", "View"}
if in.Options.Step.HasMaxSort {
importApiMethod = append(importApiMethod, "MaxSort")
}
importBuffer.WriteString(" import " + ImportWebMethod(importApiMethod) + " from '" + in.Options.ImportWebApi + "';\n")
// 导入model
var importModelMethod = []string{"State", "newState"}
if in.Options.Step.IsTreeTable {
importModelMethod = append(importModelMethod, []string{"treeOption", "loadTreeOption"}...)
}
if in.Options.Step.HasRules {
importModelMethod = append(importModelMethod, "rules")
}
importBuffer.WriteString(" import " + ImportWebMethod(importModelMethod) + " from './model';\n")
for _, field := range in.MasterFields {
if !field.IsEdit {
continue
}
switch field.FormMode {
case FMInput, FMInputTextarea, FMInputDynamic:
antdUI = append(antdUI, "Col", "FormItem", "Input", "Space")
case FMInputNumber:
antdUI = append(antdUI, "Col", "FormItem", "InputNumber")
case FMDate, FMDateRange, FMTime, FMTimeRange:
antdUI = append(antdUI, "Col", "FormItem", "DatePicker")
case FMInputEditor:
if !gstr.Contains(importBuffer.String(), `import { Tinymce as RichTextarea }`) {
importBuffer.WriteString(" import { Tinymce as RichTextarea } from '#/components/tinymce';\n")
}
antdUI = append(antdUI, "Col", "FormItem")
case FMUploadImage, FMUploadImages:
if !gstr.Contains(importBuffer.String(), `import { ImageUpload }`) {
importBuffer.WriteString(" import { ImageUpload } from '#/components/upload';\n")
}
antdUI = append(antdUI, "Col", "FormItem")
case FMUploadFile, FMUploadFiles:
if !gstr.Contains(importBuffer.String(), `import { FileUpload }`) {
importBuffer.WriteString(" import { FileUpload } from '#/components/upload';\n")
}
antdUI = append(antdUI, "Col", "FormItem")
case FMRadio:
antdUI = append(antdUI, "Col", "FormItem", "RadioGroup")
case FMCheckbox:
antdUI = append(antdUI, "Col", "FormItem", "CheckboxGroup", "Checkbox", "Space")
case FMSelect, FMSelectMultiple:
antdUI = append(antdUI, "Col", "FormItem", "Select")
case FMSwitch:
antdUI = append(antdUI, "Col", "FormItem", "Switch")
case FMRate:
antdUI = append(antdUI, "Col", "FormItem", "Rate")
case FMCitySelector, FMCascader:
antdUI = append(antdUI, "Col", "FormItem", "Cascader")
case FMPidTreeSelect, FMTreeSelect:
antdUI = append(antdUI, "Col", "FormItem", "TreeSelect")
}
}
antdUI = utility.UniqueSlice(antdUI)
if len(antdUI) > 0 {
importBuffer.WriteString(" import " + ImportWebMethod(antdUI) + " from 'ant-design-vue';\n")
}
data["import"] = importBuffer.String()
data["setup"] = ""
if in.Options.PresetStep != nil {
data["formGridSpan"] = in.Options.PresetStep.FormGridCols
}
return data
}
@@ -0,0 +1,172 @@
// package genview
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genview
import (
"bytes"
"context"
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
"github.com/gogf/gf/v2/frame/g"
)
func (l *gCurd) webIndexTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (g.Map, error) {
var (
data = make(g.Map)
importBuffer = bytes.NewBuffer(nil)
importVueMethod = []string{"h", "reactive", "ref", "computed"}
importApiMethod = []string{"List"}
importModelMethod = []string{"columns", "querySchema", "type RowType"}
importUtilsMethod []string
importIcons []string
actionWidth int64 = 72
)
// 添加 MdiPlus, MdiDelete,MdiExport
if in.Options.Step.HasAdd {
importIcons = append(importIcons, "MdiPlus")
}
// 编辑
if in.Options.Step.HasEdit {
in.Options.Step.ActionColumnWidth += actionWidth
if in.Options.Step.IsTreeTable && !in.Options.Step.IsOptionTreeTable {
in.Options.Step.ActionColumnWidth += actionWidth
}
if in.Options.Step.IsOptionTreeTable {
importIcons = append(importIcons, "MdiEdit")
}
}
// 导出
if in.Options.Step.HasExport {
importIcons = append(importIcons, "MdiExport")
importApiMethod = append(importApiMethod, "Export")
}
// 删除
if in.Options.Step.HasDel {
importApiMethod = append(importApiMethod, "Delete")
in.Options.Step.ActionColumnWidth += actionWidth
}
// 批量删除
if in.Options.Step.HasBatchDel {
importIcons = append(importIcons, "MdiDelete")
importApiMethod = append(importApiMethod, "Delete")
}
// 修改状态
if in.Options.Step.HasStatus {
importApiMethod = append(importApiMethod, "Status")
in.Options.Step.ActionColumnWidth += actionWidth
}
// 更多
// 查看详情
if in.Options.Step.HasView {
in.Options.Step.ActionColumnWidth += actionWidth
}
// 展开树
if in.Options.Step.IsTreeTable {
// importIcons = append(importIcons, "MdiExport")
}
// 存在字典数据选项
if in.Options.DictOps.Has {
importVueMethod = append(importVueMethod, "onMounted")
// importModelMethod = append(importModelMethod, "loadOptions")
}
// 选项式树表
if in.Options.Step.IsOptionTreeTable {
importVueMethod = append(importVueMethod, "onMounted")
// importIcons = append(importIcons, []string{"FormOutlined", "SearchOutlined"}...)
importModelMethod = append(importModelMethod, []string{"loadTreeOption", "treeOption", "State"}...)
}
// 操作按钮宽度最小值
if in.Options.Step.ActionColumnWidth > 0 && in.Options.Step.ActionColumnWidth < actionWidth*2 {
in.Options.Step.ActionColumnWidth = 100
}
// 导入基础包
importBuffer.WriteString(" import " + ImportWebMethod(importVueMethod) + " from 'vue';\n")
importBuffer.WriteString(" import { Button, message,Tag, Modal, Popconfirm,Switch } from 'ant-design-vue';\n")
importBuffer.WriteString(" import type { VbenFormProps } from '#/adapter/form';\n")
importBuffer.WriteString(" import type { VxeTableGridOptions, VxeGridListeners } from '#/adapter/vxe-table';\n")
importBuffer.WriteString(" import type { DeepPartial } from '@vben/types';\n")
importBuffer.WriteString(" import { getVxePopupContainer } from '@vben/utils';\n")
importBuffer.WriteString(" import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';\n")
importBuffer.WriteString(" import { useVbenVxeGrid } from '#/adapter/vxe-table';\n")
importBuffer.WriteString(" import { AccessControl, useAccess } from '@vben/access';\n")
importBuffer.WriteString(" const { hasAccessByCodes } = useAccess();\n")
if in.Options.Step.HasExport {
importBuffer.WriteString(" import { commonDownloadExcel } from '#/utils/file/download';\n")
}
// 导入字典
if in.Options.DictOps.Has {
// importBuffer.WriteString(" import { useDictStore } from '@/store/modules/dict';\n")
}
// 导入api
importBuffer.WriteString(" import " + ImportWebMethod(importApiMethod) + " from '" + in.Options.ImportWebApi + "';\n")
// 导入icons
if len(importIcons) > 0 {
importBuffer.WriteString(" import " + ImportWebMethod(importIcons) + " from '@vben/icons';\n")
}
// 导入model
if in.Options.Step.IsTreeTable {
importModelMethod = append(importModelMethod, "newState")
}
importBuffer.WriteString(" import " + ImportWebMethod(importModelMethod) + " from './model';\n")
// 导入utils
if len(importUtilsMethod) > 0 {
// importBuffer.WriteString(" import " + ImportWebMethod(importUtilsMethod) + " from '@/utils/xjadmin';\n")
}
// 导入edit组件
if in.Options.Step.HasEdit {
importBuffer.WriteString(" import editDrawer from './edit.vue';\n")
}
// 导入view组件
if in.Options.Step.HasView {
importBuffer.WriteString(" import viewDrawer from './view.vue';\n")
}
// 没有需要查询的字段则隐藏搜索表单
isSearchForm := false
for _, field := range in.MasterFields {
if field.IsQuery {
isSearchForm = true
break
}
}
if !isSearchForm {
if len(in.Options.Join) > 0 {
LoopOut:
for _, v := range in.Options.Join {
for _, column := range v.Columns {
if column.IsQuery {
isSearchForm = true
break LoopOut
}
}
}
}
}
data["isSearchForm"] = isSearchForm
data["import"] = importBuffer.String()
data["isEditModal"] = in.Options.Step.IsEditModal
return data, nil
}
@@ -0,0 +1,97 @@
// package genview
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genview
import (
"context"
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
"github.com/gogf/gf/v2/frame/g"
)
func (l *gCurd) generateWebModelDictOptions(ctx context.Context, in *genmodel.CurdPreviewParam) error {
type DictType struct {
DictType string `json:"dictType"`
DictName string `json:"dictName"`
}
var (
dictTypeIds []string
dictTypeList []*DictType
builtinDictTypeIds []int64
builtinDictTypeList []*DictType
)
for _, field := range in.MasterFields {
if field.DictType != "" {
dictTypeIds = append(dictTypeIds, field.DictType)
g.Log().Debugf(context.Background(), "curd_generate_web_model.go generateWebModelDictOptions in.masterFields:%+v", field)
}
// if field.DictType < 0 {
// builtinDictTypeIds = append(builtinDictTypeIds, field.DictType)
// }
}
g.Log().Debugf(context.Background(), "curd_generate_web_model.go generateWebModelDictOptions dictTypeIds:%+v", dictTypeIds)
dictTypeIds = utility.UniqueSlice(dictTypeIds)
builtinDictTypeIds = utility.UniqueSlice(builtinDictTypeIds)
g.Log().Debugf(context.Background(), "curd_generate_web_model.go generateWebModelDictOptions dictTypeIds:%+v", dictTypeIds)
// g.Log().Debugf(context.Background(), "curd_generate_web_model.go generateWebModelDictOptions builtinDictTypeIds:%+v", builtinDictTypeIds)
if len(dictTypeIds) == 0 && len(builtinDictTypeIds) == 0 {
return nil
}
if len(dictTypeIds) > 0 {
err := g.Model("sys_dict_type").Ctx(ctx).
Fields("dict_type", "dict_name").
WhereIn("dict_type", dictTypeIds).
Scan(&dictTypeList)
if err != nil {
return err
}
}
g.Log().Debugf(context.Background(), "curd_generate_web_model.go generateWebModelDictOptions dictTypeList:%+v", dictTypeList)
// if len(builtinDictTypeIds) > 0 {
// for _, id := range builtinDictTypeIds {
// typ, err := dict.GetTypeById(ctx, id)
// if err != nil && !errors.Is(err, dict.NotExistKeyError) {
// return err
// }
// if len(typ) > 0 {
// row := new(DictType)
// row.Id = id
// row.Type = typ
// builtinDictTypeList = append(builtinDictTypeList, row)
// }
// }
// }
if len(dictTypeList) == 0 && len(builtinDictTypeList) == 0 {
return nil
}
if len(builtinDictTypeList) > 0 {
dictTypeList = append(dictTypeList, builtinDictTypeList...)
}
in.Options.DictOps.Has = true
for _, v := range dictTypeList {
// 字段映射字典
for _, field := range in.MasterFields {
if field.DictType != "" && v.DictType == field.DictType {
in.Options.DictMap[field.TsName] = v.DictType
in.Options.DictOps.Schemas = append(in.Options.DictOps.Schemas, &genmodel.OptionsSchemasField{
Field: field.TsName,
Type: v.DictType,
})
}
}
in.Options.DictOps.Types = append(in.Options.DictOps.Types, v.DictName)
}
return nil
}
@@ -0,0 +1,131 @@
// package genview
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genview
import (
"bytes"
"context"
"fmt"
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
"github.com/gogf/gf/v2/frame/g"
)
func (l *gCurd) webViewTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) {
data = make(g.Map)
data["item"] = l.generateWebViewItem(ctx, in)
return
}
func (l *gCurd) generateWebViewItem(ctx context.Context, in *genmodel.CurdPreviewParam) string {
buffer := bytes.NewBuffer(nil)
for _, field := range in.MasterFields {
if !field.IsEdit {
continue
}
var (
defaultComponent = fmt.Sprintf(`<DescriptionsItem label="%s">{{ formValue.%s }}</DescriptionsItem>`, field.Dc, field.TsName)
component string
)
switch field.FormMode {
case FMInputTextarea, FMInputEditor:
component = fmt.Sprintf(`<DescriptionsItem label="%s">
<span v-html="formValue.%s"></span>
</DescriptionsItem>`, field.Dc, field.TsName)
case FMInputDynamic:
component = defaultComponent
case FMDate:
component = defaultComponent
case FMTime:
component = defaultComponent
case FMRadio, FMSelect:
component = fmt.Sprintf(`<DescriptionsItem label="%s">
<component :is="renderDict(String(formValue.%s), '%s')" />
</DescriptionsItem>`, field.Dc, field.TsName, in.Options.DictMap[field.TsName])
case FMCheckbox, FMSelectMultiple:
component = fmt.Sprintf(`<DescriptionsItem label="%s">
<component :is="renderDictTags(formValue.%s, '%s')" />
</DescriptionsItem>`, field.Dc, field.TsName, in.Options.DictMap[field.TsName])
case FMUploadImage:
component = fmt.Sprintf(`<DescriptionsItem label="%s">
<Image style="margin-left: 10px; height: 100px; width: 100px" :src="formValue.%s" />
</DescriptionsItem>`, field.Dc, field.TsName)
case FMUploadImages:
component = fmt.Sprintf(`<DescriptionsItem label="%s">
<Image.PreviewGroup>
<Space>
<Image
v-for="(item, key) in formValue.%s"
:key="key"
style="margin-left: 10px; height: 100px; width: 100px"
:src="item"
/>
</Space>
</Image.PreviewGroup>
</DescriptionsItem>`, field.Dc, field.TsName)
case FMUploadFile:
component = fmt.Sprintf(`<DescriptionsItem label="%s">
<div class="upload-card" v-show="formValue.%s !== ''" @click="download(formValue.%s)">
<div class="upload-card-item" style="height: 100px; width: 100px">
<div class="upload-card-item-info">
<div class="img-box">
<Avatar :style="fileAvatarCSS">{{ getFileExt(formValue.%s) }}</Avatar>
</div>
</div>
</div>
</div>
</DescriptionsItem>`, field.Dc, field.TsName, field.TsName, field.TsName)
case FMUploadFiles:
component = fmt.Sprintf(`<DescriptionsItem label="%s">
<div class="upload-card">
<Space :size="0">
<div
class="upload-card-item"
style="height: 100px; width: 100px"
v-for="(item, key) in formValue.%s"
:key="key"
>
<div class="upload-card-item-info">
<div class="img-box">
<Avatar :style="fileAvatarCSS" @click="download(item)">{{
getFileExt(item)
}}</Avatar>
</div>
</div>
</div>
</Space>
</div>
</DescriptionsItem>`, field.Dc, field.TsName)
case FMSwitch:
component = fmt.Sprintf(`<DescriptionsItem label="%s">
<Switch :checked="formValue.%s === 1" disabled />
</DescriptionsItem>`, field.Dc, field.TsName)
case FMRate:
component = fmt.Sprintf(`<DescriptionsItem label="%s">
<Rate :value="formValue.%s" disabled allow-half />
</DescriptionsItem>`, field.Dc, field.TsName)
default:
component = defaultComponent
}
buffer.WriteString(" " + component + "\n\n")
}
return buffer.String()
}
@@ -0,0 +1,331 @@
// package genview
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package genview
import (
"context"
"fmt"
"os"
"regexp"
"strings"
"unicode"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/mcache"
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_view/gohtml"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
version "git.5iot.com/iotlcm-public/iotlcm-app/utility/version"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"golang.org/x/tools/imports"
)
func replaceEmptyLinesWithSpace(input string) string {
re := regexp.MustCompile(`\n\s*\n`)
result := re.ReplaceAllString(input, "\n\n")
return result
}
func FormatTs(code string) string {
code = replaceEmptyLinesWithSpace(code)
return code + "\n"
}
func ToTSArray(vs []string) string {
formattedStrings := make([]string, len(vs))
for i, str := range vs {
formattedStrings[i] = fmt.Sprintf("'%s'", str)
}
return fmt.Sprintf("[%s]", strings.Join(formattedStrings, ", "))
}
func DictToTSArray(vs []string) string {
formattedStrings := make([]string, len(vs))
formattedObjStrings := make([]string, len(vs))
for i, str := range vs {
formattedStrings[i] = fmt.Sprintf("'%s'", str)
formattedObjStrings[i] = fmt.Sprintf("dict_%s", str)
}
return fmt.Sprintf("[%s],[%s]", strings.Join(formattedStrings, ", "), strings.Join(formattedObjStrings, ", "))
}
func RemoveSlice[K comparable](src []K, sub K) []K {
for k, v := range src {
if v == sub {
copy(src[k:], src[k+1:])
return src[:len(src)-1]
}
}
return src
}
// CheckIllegalName 检查命名是否合理
func CheckIllegalName(errPrefix string, names ...string) (err error) {
reg, _ := regexp.Compile("^[a-z_][a-z0-9_]*$")
for _, name := range names {
name = strings.ToLower(name)
match := reg.MatchString(name)
if !match {
err = gerror.Newf("%v存在格式不正确,必须全部小写且由字母、数字和下划线组成:%v", errPrefix, name)
return
}
if strings.HasSuffix(name, "test") {
err = gerror.Newf("%v当中不能以`test`结尾:%v", errPrefix, name)
return
}
if StartsWithDigit(name) {
err = gerror.Newf("%v当中不能以阿拉伯数字开头:%v", errPrefix, name)
return
}
}
return
}
func StartsWithDigit(s string) bool {
r := []rune(s)
if len(r) > 0 {
return unicode.IsDigit(r[0])
}
return false
}
// GetModName 获取主包名
func GetModName(ctx context.Context) (modName string, err error) {
if !gfile.Exists("go.mod") {
err = gerror.New("go.mod does not exist in current working directory")
return
}
var (
goModContent = gfile.GetContents("go.mod")
match, _ = gregex.MatchString(`^module\s+(.+)\s*`, goModContent)
)
if len(match) > 1 {
modName = gstr.Trim(match[1])
} else {
err = gerror.New("module name does not found in go.mod")
return
}
return
}
// parseServFunName 解析业务服务名称
func (l *gCurd) parseServFunName(templateGroup, varName string) string {
templateGroup = gstr.UcFirst(templateGroup)
if gstr.HasPrefix(varName, templateGroup) && varName != templateGroup {
return varName
}
return templateGroup + varName
}
func GetTempGeneratePath(ctx context.Context) string {
return gfile.Abs(gfile.Temp() + "/tmp_xjgen/" + version.AppName(ctx))
}
func FormatGo(ctx context.Context, name, code string) (string, error) {
path := GetTempGeneratePath(ctx) + "/" + name
if err := gfile.PutContents(path, code); err != nil {
return "", err
}
res, err := imports.Process(path, []byte(code), nil)
if err != nil {
err = gerror.Newf(`FormatGo error format "%s" go files: %v`, path, err)
return "", err
}
return string(res), nil
}
func formatComment(comment string) string {
comment = gstr.ReplaceByArray(comment, g.SliceStr{
"\n", " ",
"\r", " ",
})
comment = gstr.Replace(comment, `\n`, " ")
comment = gstr.Trim(comment)
return comment
}
func removeEndWrap(comment string) string {
if len(comment) > 2 && comment[len(comment)-2:] == " \n" {
comment = comment[:len(comment)-2]
}
return comment
}
func isEffectiveJoin(join *genmodel.CurdOptionsJoin) bool {
return join.Alias != "" && join.Field != "" && join.LinkTable != "" && join.MasterField != "" && join.DaoName != "" && join.LinkMode > 0
}
func IsPidName(name string) bool {
return name == "pid"
}
// CamelCaseToUnderline 驼峰单词转下划线单词
func CamelCaseToUnderline(s string) string {
var output []rune
for i, r := range s {
if i == 0 {
output = append(output, unicode.ToLower(r))
} else {
if unicode.IsUpper(r) {
output = append(output, '_')
}
output = append(output, unicode.ToLower(r))
}
}
return string(output)
}
// hasEffectiveJoin 存在有效的关联表
func hasEffectiveJoins(joins []*genmodel.CurdOptionsJoin) bool {
for _, join := range joins {
if isEffectiveJoin(join) {
return true
}
}
return false
}
// ImportWebMethod 导入前端方法
func ImportWebMethod(vs []string) string {
vs = utility.UniqueSlice(vs)
str := "{ " + strings.Join(vs, ", ") + " }"
str = strings.TrimSuffix(str, ", ")
return str
}
func FormatVue(code string) string {
endTag := `</template>`
vueLen := gstr.PosR(code, endTag)
vueCode := code[:vueLen+len(endTag)]
tsCode := code[vueLen+len(endTag):]
vueCode = gohtml.Format(vueCode)
tsCode = FormatTs(tsCode)
return vueCode + tsCode
}
// ImportSql 导出sql文件
func ImportSql(ctx context.Context, path string) error {
rows, err := os.ReadFile(path)
if err != nil {
return err
}
beforeMaxMenuId, _ := dao.SysMenu.Ctx(ctx).Max(dao.SysMenu.Columns().MenuId)
sqlArr := strings.Split(string(rows), "\n")
for _, sql := range sqlArr {
sql = strings.TrimSpace(sql)
if sql == "" || strings.HasPrefix(sql, "--") {
continue
}
g.Log().Infof(ctx, "views.ImportSql sql:%v", sql)
exec, err := g.DB().Exec(ctx, sql)
g.Log().Infof(ctx, "views.ImportSql sql:%v, exec:%+v, err:%+v", sql, exec, err)
if err != nil {
return err
}
}
if err = syncGeneratedMenus(ctx, gconv.Int64(beforeMaxMenuId)); err != nil {
return err
}
return nil
}
func syncGeneratedMenus(ctx context.Context, beforeMaxMenuId int64) error {
var menuIds []int64
if err := dao.SysMenu.Ctx(ctx).
Fields(dao.SysMenu.Columns().MenuId).
WhereGT(dao.SysMenu.Columns().MenuId, beforeMaxMenuId).
OrderAsc(dao.SysMenu.Columns().MenuId).
Scan(&menuIds); err != nil {
return err
}
if len(menuIds) == 0 {
return nil
}
for _, menuId := range menuIds {
if err := service.MemDBSysMenu().LoadData(ctx, menuId); err != nil {
return err
}
}
var roleIds []int64
if err := dao.SysRoleMenu.Ctx(ctx).
Fields(dao.SysRoleMenu.Columns().RoleId).
WhereIn(dao.SysRoleMenu.Columns().MenuId, menuIds).
Group(dao.SysRoleMenu.Columns().RoleId).
Scan(&roleIds); err != nil {
return err
}
if len(roleIds) == 0 {
return nil
}
var userIds []int64
if err := dao.SysUserRole.Ctx(ctx).
Fields(dao.SysUserRole.Columns().UserId).
WhereIn(dao.SysUserRole.Columns().RoleId, roleIds).
Group(dao.SysUserRole.Columns().UserId).
Scan(&userIds); err != nil {
return err
}
for _, userId := range userIds {
_ = mcache.RemoveUserAccessCodeList(ctx, userId)
_ = mcache.RemoveUserRoleDataAccessCodeList(ctx, userId)
}
return nil
}
// ParseDBConfigNodeLink 解析数据库连接配置
func ParseDBConfigNodeLink(node *gdb.ConfigNode) *gdb.ConfigNode {
const linkPattern = `(\w+):([\w\-\$]*):(.*?)@(\w+?)\((.+?)\)/{0,1}([^\?]*)\?{0,1}(.*)`
const defaultCharset = `utf8`
const defaultProtocol = `tcp`
var match []string
if node.Link != "" {
match, _ = gregex.MatchString(linkPattern, node.Link)
if len(match) > 5 {
node.Type = match[1]
node.User = match[2]
node.Pass = match[3]
node.Protocol = match[4]
array := gstr.Split(match[5], ":")
if len(array) == 2 && node.Protocol != "file" {
node.Host = array[0]
node.Port = array[1]
node.Name = match[6]
} else {
node.Name = match[5]
}
if len(match) > 6 && match[7] != "" {
node.Extra = match[7]
}
node.Link = ""
}
}
if node.Extra != "" {
if m, _ := gstr.Parse(node.Extra); len(m) > 0 {
_ = gconv.Struct(m, &node)
}
}
// Default value checks.
if node.Charset == "" {
node.Charset = defaultCharset
}
if node.Protocol == "" {
node.Protocol = defaultProtocol
}
return node
}
@@ -0,0 +1,12 @@
// package gohtml
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package gohtml
const (
defaultIndentString = " "
startIndent = 0
defaultLastElement = "</html>"
)
@@ -0,0 +1,6 @@
// package genview
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package gohtml
@@ -0,0 +1,12 @@
// package gohtml
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package gohtml
// An element represents an HTML element.
type element interface {
isInline() bool
write(*formattedBuffer, bool) bool
}
@@ -0,0 +1,41 @@
// package gohtml
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package gohtml
import (
"bytes"
"strconv"
"strings"
)
// Format parses the input HTML string, formats it and returns the result.
func Format(s string) string {
return parse(strings.NewReader(s)).html()
}
// FormatBytes parses input HTML as bytes, formats it and returns the result.
func FormatBytes(b []byte) []byte {
return parse(bytes.NewReader(b)).bytes()
}
// Format parses the input HTML string, formats it and returns the result with line no.
func FormatWithLineNo(s string) string {
return AddLineNo(Format(s))
}
func AddLineNo(s string) string {
lines := strings.Split(s, "\n")
maxLineNoStrLen := len(strconv.Itoa(len(lines)))
bf := &bytes.Buffer{}
for i, line := range lines {
lineNoStr := strconv.Itoa(i + 1)
if i > 0 {
bf.WriteString("\n")
}
bf.WriteString(strings.Repeat(" ", maxLineNoStrLen-len(lineNoStr)) + lineNoStr + " " + line)
}
return bf.String()
}
@@ -0,0 +1,59 @@
// package gohtml
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package gohtml
import (
"bytes"
"regexp"
)
// Column to wrap lines to (disabled by default)
var LineWrapColumn = 0
// Maxmimum characters a long word can extend past LineWrapColumn without wrapping
var LineWrapMaxSpillover = 5
// An htmlDocument represents an HTML document.
type htmlDocument struct {
elements []element
}
// html generates an HTML source code and returns it.
func (htmlDoc *htmlDocument) html() string {
str := string(htmlDoc.bytes())
str = replaceMultipleNewlinesWithSpaceAndTabs(str)
return str
}
func replaceMultipleNewlinesWithSpaceAndTabs(input string) string {
re := regexp.MustCompile(`\n\s*\n+`)
formattedString := re.ReplaceAllString(input, "\n")
return formattedString
}
// bytes reads from htmlDocument's internal array of elements and returns HTML source code
func (htmlDoc *htmlDocument) bytes() []byte {
bf := &formattedBuffer{
buffer: &bytes.Buffer{},
lineWrapColumn: LineWrapColumn,
lineWrapMaxSpillover: LineWrapMaxSpillover,
indentString: defaultIndentString,
indentLevel: startIndent,
}
isPreviousNodeInline := true
for _, child := range htmlDoc.elements {
isPreviousNodeInline = child.write(bf, isPreviousNodeInline)
}
return bf.buffer.Bytes()
}
// append appends an element to the htmlDocument.
func (htmlDoc *htmlDocument) append(e element) {
htmlDoc.elements = append(htmlDoc.elements, e)
}
@@ -0,0 +1,133 @@
// package gohtml
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package gohtml
import (
"io"
"regexp"
"strings"
"golang.org/x/net/html"
)
// parse parses a stirng and converts it into an html.
func parse(r io.Reader) *htmlDocument {
htmlDoc := &htmlDocument{}
tokenizer := html.NewTokenizer(r)
for {
if errorToken, _, _ := parseToken(tokenizer, htmlDoc, nil); errorToken {
break
}
}
return htmlDoc
}
// Function that identifies which tags will be treated as containing preformatted
// content. Such tags will have the formatting of all its contents preserved
// unchanged.
// The opening tag html.Token is passed to this function.
// By default, only <pre> and <textarea> tags are considered preformatted.
var IsPreformatted = func(token html.Token) bool {
return token.Data == "pre" || token.Data == "textarea"
}
func parseToken(tokenizer *html.Tokenizer, htmlDoc *htmlDocument, parent *tagElement) (bool, bool, string) {
tokenType := tokenizer.Next()
raw := string(tokenizer.Raw())
switch tokenType {
case html.ErrorToken:
return true, false, ""
case html.TextToken:
text := string(tokenizer.Raw())
if strings.TrimSpace(text) == "" && (parent == nil || !parent.isRaw) {
break
}
textElement := &textElement{text: text, parent: parent}
appendElement(htmlDoc, parent, textElement)
case html.StartTagToken:
raw := string(tokenizer.Raw())
token := tokenizer.Token()
tagElement := &tagElement{
tagName: string(token.Data),
startTagRaw: raw,
isRaw: IsPreformatted(token) || (parent != nil && parent.isRaw),
parent: parent,
}
appendElement(htmlDoc, parent, tagElement)
for {
errorToken, parentEnded, unsetEndTag := parseToken(tokenizer, htmlDoc, tagElement)
if errorToken {
return true, false, ""
}
if parentEnded {
if unsetEndTag != "" {
return false, false, unsetEndTag
}
break
}
if unsetEndTag != "" {
tagName := setEndTagRaw(tokenizer, tagElement, unsetEndTag)
return false, false, tagName
}
}
case html.EndTagToken:
tagName := setEndTagRaw(tokenizer, parent, getTagName(tokenizer))
return false, true, tagName
case html.DoctypeToken, html.CommentToken:
tagElement := &tagElement{
tagName: getTagName(tokenizer),
startTagRaw: string(tokenizer.Raw()),
isRaw: parent != nil && parent.isRaw,
parent: parent,
}
appendElement(htmlDoc, parent, tagElement)
case html.SelfClosingTagToken:
tagElement := &tagElement{
tagName: getTagName(tokenizer),
startTagRaw: raw,
isRaw: parent != nil && parent.isRaw,
parent: parent,
}
appendElement(htmlDoc, parent, tagElement)
}
return false, false, ""
}
// appendElement appends the element to the htmlDocument or parent tagElement.
func appendElement(htmlDoc *htmlDocument, parent *tagElement, e element) {
if parent != nil {
parent.appendChild(e)
} else {
htmlDoc.append(e)
}
}
// getTagName gets a tagName from tokenizer.
func getTagName(tokenizer *html.Tokenizer) string {
tagName, _ := tokenizer.TagName()
return string(tagName)
}
// setEndTagRaw sets an endTagRaw to the parent.
func setEndTagRaw(_ *html.Tokenizer, parent *tagElement, tagName string) string {
if parent != nil && parent.tagName == tagName {
parent.endTagRaw = `</` + fMustCompile(parent.startTagRaw) + `>` // string(tokenizer.Raw())
return ""
}
return tagName
}
func fMustCompile(input string) (result string) {
re := regexp.MustCompile(`<([A-Za-z-]+)[\s\S]*?>`)
match := re.FindStringSubmatch(input)
if len(match) > 1 {
result = match[1]
} else {
result = input
}
return
}
@@ -0,0 +1,150 @@
// package gohtml
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package gohtml
import (
"bytes"
)
// A tagElement represents a tag element of an HTML document.
type tagElement struct {
tagName string
startTagRaw string
endTagRaw string
parent *tagElement
children []element
isRaw bool
isChildrenInlineCache *bool
}
// Enable condensing a tag with only inline children onto a single line, or
// completely inlining it with sibling nodes.
// Tags to be treated as inline can be set in `InlineTags`.
// Only inline tags will be completely inlined, while other condensable tags
// will be given their own dedicated (single) line.
var Condense bool
// Tags that are considered inline tags.
// Note: Text nodes are always considered to be inline
var InlineTags = map[string]bool{
"a": true,
"code": true,
"em": true,
"span": true,
"strong": true,
}
// Maximum length of an opening inline tag before it's un-inlined
var InlineTagMaxLength = 40
func (e *tagElement) isInline() bool {
if e.isRaw || !InlineTags[e.tagName] || len(e.startTagRaw) > InlineTagMaxLength {
return false
}
return e.isChildrenInline()
}
func (e *tagElement) isChildrenInline() bool {
if !Condense {
return false
}
if e.isChildrenInlineCache != nil {
return *e.isChildrenInlineCache
}
isInline := true
for _, child := range e.children {
isInline = isInline && child.isInline()
}
e.isChildrenInlineCache = &isInline
return isInline
}
// write writes a tag to the buffer.
func (e *tagElement) write(bf *formattedBuffer, isPreviousNodeInline bool) bool {
if e.isRaw {
if e.parent != nil && !e.parent.isRaw {
bf.writeLineFeed()
bf.writeIndent()
bf.rawMode = true
defer func() {
bf.rawMode = false
}()
}
bf.writeToken(e.startTagRaw, formatterTokenType_Tag)
for _, child := range e.children {
child.write(bf, true)
}
bf.writeToken(e.endTagRaw, formatterTokenType_Tag)
return false
}
if e.isChildrenInline() && (e.endTagRaw != "" || e.isInline()) {
// Write the condensed output to a separate buffer, in case it doesn't work out
condensedBuffer := *bf
condensedBuffer.buffer = &bytes.Buffer{}
if bf.buffer.Len() > 0 && (!isPreviousNodeInline || !e.isInline()) {
condensedBuffer.writeLineFeed()
}
condensedBuffer.writeToken(e.startTagRaw, formatterTokenType_Tag)
if !isPreviousNodeInline && e.endTagRaw != "" {
condensedBuffer.indentLevel++
}
for _, child := range e.children {
child.write(&condensedBuffer, true)
}
if e.endTagRaw != "" {
condensedBuffer.writeToken(e.endTagRaw, formatterTokenType_Tag)
if !isPreviousNodeInline {
condensedBuffer.indentLevel--
}
}
if e.isInline() || bytes.ContainsAny(condensedBuffer.buffer.Bytes()[1:], "\n") {
// If we're an inline tag, or there were no newlines were in the buffer,
// replace the original with the condensed version
condensedBuffer.buffer = bytes.NewBuffer(bytes.Join([][]byte{
bf.buffer.Bytes(), condensedBuffer.buffer.Bytes(),
}, []byte{}))
*bf = condensedBuffer
return e.isInline()
}
}
if bf.buffer.Len() > 0 {
bf.writeLineFeed()
}
bf.writeToken(e.startTagRaw, formatterTokenType_Tag)
if e.endTagRaw != "" {
bf.indentLevel++
}
isPreviousNodeInline = false
for _, child := range e.children {
isPreviousNodeInline = child.write(bf, isPreviousNodeInline)
}
if e.endTagRaw != "" {
if len(e.children) > 0 {
bf.writeLineFeed()
}
bf.indentLevel--
bf.writeToken(e.endTagRaw, formatterTokenType_Tag)
}
return false
}
// appendChild append an element to the element's children.
func (e *tagElement) appendChild(child element) {
e.children = append(e.children, child)
}
@@ -0,0 +1,48 @@
// package gohtml
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package gohtml
import (
"regexp"
"strings"
)
// A textElement represents a text element of an HTML document.
type textElement struct {
text string
parent *tagElement
}
func (e *textElement) isInline() bool {
// Text nodes are always considered to be inline
return true
}
// write writes a text to the buffer.
func (e *textElement) write(bf *formattedBuffer, isPreviousNodeInline bool) bool {
text := unifyLineFeed(e.text)
if e.parent != nil && e.parent.isRaw {
bf.writeToken(text, formatterTokenType_Text)
return true
}
if !isPreviousNodeInline {
bf.writeLineFeed()
}
// Collapse leading and trailing spaces
text = regexp.MustCompile(`^\s+|\s+$`).ReplaceAllString(text, " ")
lines := strings.Split(text, "\n")
for l, line := range lines {
if l > 0 {
bf.writeLineFeed()
}
for _, word := range strings.Split(line, " ") {
bf.writeToken(word, formatterTokenType_Text)
}
}
return true
}
@@ -0,0 +1,106 @@
// package gohtml
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package gohtml
import (
"bytes"
"strings"
"unicode"
)
type formatterTokenType int
const (
formatterTokenType_Nothing formatterTokenType = iota
formatterTokenType_Tag
formatterTokenType_Text
)
type formattedBuffer struct {
buffer *bytes.Buffer
rawMode bool
indentString string
indentLevel int
lineWrapColumn int
lineWrapMaxSpillover int
curLineLength int
prevTokenType formatterTokenType
}
func (bf *formattedBuffer) writeLineFeed() {
if !bf.rawMode {
// Strip trailing newlines
bf.buffer = bytes.NewBuffer(bytes.TrimRightFunc(
bf.buffer.Bytes(),
func(r rune) bool {
return r != '\n' && unicode.IsSpace(r)
},
))
}
bf.buffer.WriteString("\n")
bf.curLineLength = 0
bf.prevTokenType = formatterTokenType_Nothing
}
func (bf *formattedBuffer) writeIndent() {
bf.buffer.WriteString(strings.Repeat(bf.indentString, bf.indentLevel))
bf.curLineLength += len(bf.indentString) * bf.indentLevel
}
func (bf *formattedBuffer) writeToken(token string, kind formatterTokenType) {
if bf.rawMode {
bf.buffer.WriteString(token)
bf.curLineLength += len(token)
return
}
if bf.prevTokenType == formatterTokenType_Nothing && strings.TrimSpace(token) == "" {
// It's a whitespace token, but we already have indentation which functions
// the same, so we ignore it
return
}
toWrite := token
if kind == formatterTokenType_Text && bf.prevTokenType == formatterTokenType_Text {
toWrite = " " + token
}
if bf.prevTokenType != formatterTokenType_Nothing && bf.lineWrapColumn > 0 {
switch {
case bf.curLineLength > bf.lineWrapColumn:
// Current line is too long
fallthrough
case bf.curLineLength+len(toWrite) > bf.lineWrapColumn+bf.lineWrapMaxSpillover:
// Current line + new token is too long even with allowed spillover
fallthrough
case bf.curLineLength+len(toWrite) > bf.lineWrapColumn &&
bf.curLineLength > bf.lineWrapColumn-bf.lineWrapMaxSpillover:
// Current line + new token is too long and doesn't quality for spillover
bf.writeLineFeed()
bf.writeToken(token, kind)
return
}
}
if bf.curLineLength == 0 {
bf.writeIndent()
}
bf.buffer.WriteString(toWrite)
bf.curLineLength += len(toWrite)
bf.prevTokenType = kind
}
// unifyLineFeed unifies line feeds.
func unifyLineFeed(s string) string {
return strings.ReplaceAll(strings.ReplaceAll(s, "\r\n", "\n"), "\r", "\n")
}
@@ -0,0 +1,38 @@
// package gohtml
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package gohtml
import (
"bytes"
"io"
)
// A Writer represents a formatted HTML source codes writer.
type Writer struct {
writer io.Writer
lastElement string
bf *bytes.Buffer
}
// SetLastElement set the lastElement to the Writer.
func (wr *Writer) SetLastElement(lastElement string) *Writer {
wr.lastElement = lastElement
return wr
}
// Write writes the parameter.
func (wr *Writer) Write(p []byte) (n int, err error) {
n, _ = wr.bf.Write(p) // (*bytes.Buffer).Write never produces an error
if bytes.HasSuffix(p, []byte(wr.lastElement)) {
_, err = wr.writer.Write([]byte(Format(wr.bf.String()) + "\n"))
}
return n, err
}
// NewWriter generates a Writer and returns it.
func NewWriter(wr io.Writer) *Writer {
return &Writer{writer: wr, lastElement: defaultLastElement, bf: &bytes.Buffer{}}
}
+484
View File
@@ -0,0 +1,484 @@
// package xgen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package xgen
import (
"context"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/addons"
genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts"
gendao "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_dao"
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
genview "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_view"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"sort"
"strings"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gproc"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
)
const gfCliVersion = "v2.9.6"
func gfCliCommand(args ...string) string {
return "go run github.com/gogf/gf/cmd/gf/v2@" + gfCliVersion + " " + strings.Join(args, " ")
}
func GenTypeSelects(ctx context.Context) (output []*genmodel.SelectGenTypeModel, err error) {
output = make([]*genmodel.SelectGenTypeModel, 0)
for k, v := range genconsts.GenCodesTypeNameMap {
item := &genmodel.SelectGenTypeModel{
SelectItemModel: genmodel.SelectItemModel{
Value: k,
Name: v,
Label: v,
Disabled: false,
},
Templates: make([]*genmodel.SelectGenTemplateModel, 0),
}
confName, ok := genconsts.GenCodesTypeConfMap[k]
if !ok {
continue
}
confTemplates := make([]*genmodel.GenCodesConfigCrudTemplate, 0)
err = g.Cfg().MustGet(ctx, "xgen.application."+confName+".templates").Scan(&confTemplates)
if err != nil {
return
}
if len(confTemplates) == 0 && confName != "crud" {
err = g.Cfg().MustGet(ctx, "xgen.application.crud.templates").Scan(&confTemplates)
if err != nil {
return
}
}
for index, v := range confTemplates {
item.Templates = append(item.Templates, &genmodel.SelectGenTemplateModel{
SelectItemModel: genmodel.SelectItemModel{
Value: index,
Label: v.Group,
Name: v.Group,
Disabled: false,
},
IsAddon: v.IsAddon,
})
}
sort.Slice(item.Templates, func(i, j int) bool {
return item.Templates[i].Value.(int) < item.Templates[j].Value.(int)
})
output = append(output, item)
}
return
}
func DbSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
output = make([]*genmodel.SelectItemModel, 0)
dbs := g.Cfg().MustGet(ctx, "xgen.selectDbs")
if len(dbs.Strings()) == 0 {
return
}
for _, v := range dbs.Strings() {
output = append(output, &genmodel.SelectItemModel{
Value: v,
Label: v,
Name: v,
Disabled: false,
})
}
return
}
func StatusSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
output = make([]*genmodel.SelectItemModel, 0)
for k, v := range genconsts.GenCodesStatusNameMap {
output = append(output, &genmodel.SelectItemModel{
Value: k,
Label: v,
Name: v,
Disabled: false,
})
}
sort.Slice(output, func(i, j int) bool {
return output[i].Value.(string) < output[j].Value.(string)
})
return
}
func LinkModeSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
output = make([]*genmodel.SelectItemModel, 0)
for k, v := range genconsts.GenCodesJoinNameMap {
output = append(output, &genmodel.SelectItemModel{
Value: k,
Label: v,
Name: v,
Disabled: false,
})
}
sort.Slice(output, func(i, j int) bool {
return output[i].Value.(int) < output[j].Value.(int)
})
return
}
func BuildMethodSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
output = make([]*genmodel.SelectItemModel, 0)
for k, v := range genconsts.GenCodesBuildMethNameMap {
output = append(output, &genmodel.SelectItemModel{
Value: k,
Label: v,
Name: v,
Disabled: false,
})
}
sort.Slice(output, func(i, j int) bool {
return output[i].Value.(int) < output[j].Value.(int)
})
return
}
func FormModeSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
output = make([]*genmodel.SelectItemModel, 0)
for _, v := range genview.FMs {
output = append(output, &genmodel.SelectItemModel{
Value: v,
Label: genview.FMMap[v],
Name: genview.FMMap[v],
Disabled: false,
})
}
return
}
func FormRoleSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
output = make([]*genmodel.SelectItemModel, 0)
for k, v := range genview.FRMap {
output = append(output, &genmodel.SelectItemModel{
Value: k,
Label: v,
Name: v,
Disabled: false,
})
}
return
}
func DictModeSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
dictMode, _, err := service.SysDictType().List(ctx, &model.SysDictTypeListParam{
PageInfo: request.PageInfo{
Page: 1,
PageSize: 2000,
},
})
if err != nil {
return
}
for _, v := range dictMode {
output = append(output, &genmodel.SelectItemModel{
Value: v.DictType,
Label: v.DictName + "(" + v.DictType + ")",
Name: v.DictName,
})
}
return
}
func WhereModeSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
output = make([]*genmodel.SelectItemModel, 0)
for _, v := range genview.WMs {
output = append(output, &genmodel.SelectItemModel{
Value: v,
Label: v,
Name: v,
Disabled: false,
})
}
return
}
func TableAlignSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
output = make([]*genmodel.SelectItemModel, 0)
for _, v := range genview.TableAligns {
output = append(output, &genmodel.SelectItemModel{
Value: v,
Label: genview.TableAlignMap[v],
Name: genview.TableAlignMap[v],
Disabled: false,
})
}
return
}
func TreeStyleTypeSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
output = make([]*genmodel.SelectItemModel, 0)
for k, v := range genconsts.GenCodesTreeStyleTypeNameMap {
output = append(output, &genmodel.SelectItemModel{
Value: k,
Label: v,
Name: v,
Disabled: false,
})
}
sort.Slice(output, func(i, j int) bool {
return output[i].Value.(int) < output[j].Value.(int)
})
return
}
func GenCodesSelects(ctx context.Context) (output *genmodel.SelectsModel, err error) {
output = &genmodel.SelectsModel{}
output.GenType, err = GenTypeSelects(ctx)
if err != nil {
return
}
output.Db, err = DbSelects(ctx)
if err != nil {
return
}
output.Status, err = StatusSelects(ctx)
if err != nil {
return
}
output.LinkMode, err = LinkModeSelects(ctx)
if err != nil {
return
}
output.BuildMethod, err = BuildMethodSelects(ctx)
if err != nil {
return
}
output.FormMode, err = FormModeSelects(ctx)
if err != nil {
return
}
output.WhereMode, err = WhereModeSelects(ctx)
if err != nil {
return
}
output.FormRole, err = FormRoleSelects(ctx)
if err != nil {
return
}
output.DictMode, err = DictModeSelects(ctx)
if err != nil {
return
}
output.Addons, err = addons.ModuleSelects(ctx)
if err != nil {
return
}
output.TableAlign, err = TableAlignSelects(ctx)
if err != nil {
return
}
output.TreeStyleType, err = TreeStyleTypeSelects(ctx)
if err != nil {
return
}
return
}
func GenCodesTableSelect(ctx context.Context, params *genmodel.GenCodesTableSelectParam) (output []*genmodel.GenCodesTableSelectModel, err error) {
if params == nil || params.DbGroup == "" {
return
}
sql := "SELECT TABLE_NAME as value, TABLE_COMMENT as label FROM information_schema.`TABLES` WHERE TABLE_SCHEMA = ?"
config := g.DB(params.DbGroup).GetConfig()
disableTables := g.Cfg().MustGet(ctx, "xgen.disableTables").Strings()
lists := make([]*genmodel.GenCodesTableSelectModel, 0)
err = g.DB(params.DbGroup).Ctx(ctx).Raw(sql, config.Name).Scan(&lists)
if err != nil {
return
}
patternStr := `addon_(\w+)_`
repStr := ``
for _, v := range lists {
if gstr.InArray(disableTables, v.Value) {
continue
}
newValue := v.Value
if config.Prefix != "" {
newValue = gstr.SubStrFromEx(v.Value, config.Prefix)
}
// g.Log().Infof(ctx, "server/internal/library/xgen/xgen_curd.go GenCodesTableSelect newValue:%v config.Prefix:%v", newValue, config.Prefix)
if newValue == "" {
continue
}
// 插件移除掉插件表前缀
bt, err := gregex.Replace(patternStr, []byte(repStr), []byte(newValue))
if err != nil {
continue
}
// g.Log().Infof(ctx, "server/internal/library/xgen/xgen_curd.go GenCodesTableSelect newValue:%v string(bt):%v", newValue, string(bt))
row := v
row.DefTableComment = v.Label
row.DaoName = gstr.CaseCamel(newValue)
row.DefVarName = gstr.CaseCamel(string(bt))
row.DefAlias = gstr.CaseCamelLower(newValue)
row.Name = fmt.Sprintf("%s (%s)", v.Value, v.Label)
row.Label = row.Name
output = append(output, row)
}
return
}
func GenCodesColumnSelect(ctx context.Context, params *genmodel.GenCodesColumnSelectParam) (output []*genmodel.GenCodesColumnSelectModel, err error) {
sql := "SELECT COLUMN_NAME as value, COLUMN_COMMENT as label FROM information_schema.`COLUMNS` WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?"
dbConfig := g.DB(params.DbGroup).GetConfig()
lists := make([]*genmodel.GenCodesColumnSelectModel, 0)
err = g.DB(params.DbGroup).Ctx(ctx).Raw(sql, dbConfig.Name, params.TableName).Scan(&lists)
if err != nil {
return
}
if len(lists) == 0 {
err = gerror.New("表不存在")
return
}
for _, v := range lists {
output = append(output, &genmodel.GenCodesColumnSelectModel{
Value: v.Value,
Label: v.Label,
Name: fmt.Sprintf("%s (%s)", v.Value, v.Label),
})
}
return
}
func GenCodesColumnList(ctx context.Context, params *genmodel.GenCodesColumnListParam) (output []*genmodel.GenCodesColumnListModel, err error) {
output, err = genview.TableColumnList(ctx, params)
return
}
// GetLoadGenerate 获取本地生成配置
func GetLoadGenerate(ctx context.Context) (conf *genmodel.GenCodesConfig, err error) {
err = g.Cfg().MustGet(ctx, "xgen").Scan(&conf)
return
}
func GenCodesPreview(ctx context.Context, params *genmodel.GenCodesPreviewParam) (output *genmodel.GenCodesPreviewModel, err error) {
genConfig, err := GetLoadGenerate(ctx)
if err != nil {
return nil, err
}
daoConfig, err := gendao.GetDaoConfig(ctx, params.DbName)
if err != nil {
return nil, err
}
switch params.GenType {
case genconsts.GenCodesTypeCurd, genconsts.GenCodesTypeTree:
return genview.Curd.DoPreview(ctx, &genmodel.CurdPreviewParam{
In: params,
DaoConfig: daoConfig,
Config: genConfig,
})
default:
err = gerror.Newf("生成类型暂不支持!")
return
}
}
// Build 提交生成
func GenCodesBuild(ctx context.Context, params *genmodel.GenCodesBuildParam) (output *genmodel.GenCodesBuildModel, err error) {
output = &genmodel.GenCodesBuildModel{}
genConfig, err := GetLoadGenerate(ctx)
if err != nil {
return nil, err
}
daoConfig, err := gendao.GetDaoConfig(ctx, params.DbName)
if err != nil {
return nil, err
}
switch params.GenType {
case genconsts.GenCodesTypeCurd, genconsts.GenCodesTypeTree:
pin := params
return output, genview.Curd.DoBuild(ctx, &genmodel.CurdBuildParam{
PreviewIn: &genmodel.CurdPreviewParam{
In: pin,
DaoConfig: daoConfig,
Config: genConfig,
},
BeforeEvent: genmodel.CurdBuildEvent{"runDao": Dao},
AfterEvent: genmodel.CurdBuildEvent{"runService": func(ctx context.Context) (err error) {
cfg := gendao.GetServiceConfig()
// 插件模块,切换到插件下运行gen service
if genConfig.Application.Crud.Templates[pin.GenTemplate].IsAddon {
// 依然使用配置中的参数,只是将生成路径指向插件模块路径
cfg.SrcFolder = "addons/" + pin.AddonName + "/logic"
cfg.DstFolder = "addons/" + pin.AddonName + "/service"
}
err = ServiceWithCfg(ctx, cfg)
return
}},
})
default:
err = gerror.Newf("生成类型暂不支持!")
return
}
}
// ServiceWithCfg 生成业务接口
func ServiceWithCfg(ctx context.Context, cfg ...*genmodel.GenServiceConfig) (err error) {
c := gendao.GetServiceConfig()
if len(cfg) > 0 {
c = cfg[0]
}
cmd := gfCliCommand("gen", "service")
if c.SrcFolder != "" {
cmd += ` -s ` + c.SrcFolder
}
if c.DstFolder != "" {
cmd += ` -d ` + c.DstFolder
}
if c.DstFileNameCase != "" {
cmd += ` -f ` + c.DstFileNameCase
}
if c.WatchFile != "" {
cmd += ` -w ` + c.WatchFile
}
if c.StPattern != "" {
cmd += ` -a ` + c.StPattern
}
// Do not pass "-l" here. GoFrame v2.9.6 can mis-handle relative paths
// on Windows and remove the generated service files it just created.
if c.Clear {
g.Log().Warning(ctx, "gf gen service clear is disabled on Windows to avoid deleting generated files")
}
if c.ImportPrefix != "" {
cmd += ` -i ` + c.ImportPrefix
}
if c.Packages != nil {
cmd += ` -p ` + strings.Join(c.Packages, ",")
}
r, err := gproc.ShellExec(gctx.New(), cmd)
if err != nil {
return err
}
g.Log().Info(ctx, "生成业务接口 Stdout", r)
return
}
// Dao 生成数据库实体
func Dao(ctx context.Context) (err error) {
cmd := gfCliCommand("gen", "dao")
r, err := gproc.ShellExec(gctx.New(), cmd)
if err != nil {
g.Log().Error(ctx, "生成数据库实体 Stderr", r)
return err
}
g.Log().Info(ctx, "生成数据库实体 Stdout", r)
return
}
+19
View File
@@ -0,0 +1,19 @@
// package xgen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package xgen
import (
"context"
gendao "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_dao"
)
func Init(ctx context.Context) (err error) {
err = gendao.Init(ctx)
if err != nil {
return
}
return
}