([\n")
+
+ // 主表
+ l.generateWebModelFormSchemaEach(buffer, in.masterFields, in)
+
+ // 关联表
+ if len(in.options.Join) > 0 {
+ for _, v := range in.options.Join {
+ if !isEffectiveJoin(v) {
+ continue
+ }
+ l.generateWebModelFormSchemaEach(buffer, v.Columns, in)
+ }
+ }
+
+ buffer.WriteString("]);\n")
+ return buffer.String()
+}
+
+// generateWebDictOption 生产字典选项
+func (l *gCurd) generateWebDictOption(typ any) string {
+ return fmt.Sprintf(`dict.getOption('%v')`, typ)
+}
+
+func (l *gCurd) generateWebModelFormSchemaEach(buffer *bytes.Buffer, fields []*sysin.GenCodesColumnListModel, in *CurdPreviewInput) {
+ for _, field := range fields {
+ if !field.IsQuery {
+ continue
+ }
+ in.options.Step.HasSearchForm = true
+
+ // 查询用户摘要
+ if field.IsQuery && in.options.Step.HasQueryMemberSummary && IsMemberSummaryField(field.Name) {
+ buffer.WriteString(fmt.Sprintf(" {\n field: '%s',\n component: '%s',\n label: '%s',\n componentProps: {\n placeholder: '请输入ID|用户名|姓名|手机号',\n onUpdateValue: (e: any) => {\n console.log(e);\n },\n },\n },\n", field.TsName, "NInput", field.Dc))
+ continue
+ }
+
+ var (
+ defaultComponent = fmt.Sprintf(" {\n field: '%s',\n component: '%s',\n label: '%s',\n componentProps: {\n placeholder: '请输入%s',\n onUpdateValue: (e: any) => {\n console.log(e);\n },\n },\n },\n", field.TsName, "NInput", field.Dc, field.Dc)
+ component string
+ )
+
+ // 这里根据编辑表单组件来进行推断,如果没有则使用默认input,这可能会导致和查询条件所需参数不符的情况
+ switch field.FormMode {
+ case FormModeInput, FormModeInputTextarea, FormModeInputEditor, FormModeInputYaml:
+ component = defaultComponent
+
+ case FormModeInputNumber:
+ component = fmt.Sprintf(" {\n field: '%s',\n component: '%s',\n label: '%s',\n componentProps: {\n placeholder: '请输入%s',\n onUpdateValue: (e: any) => {\n console.log(e);\n },\n },\n },\n", field.TsName, "NInputNumber", field.Dc, field.Dc)
+
+ case FormModeDate:
+ component = fmt.Sprintf(" {\n field: '%s',\n component: '%s',\n label: '%s',\n componentProps: {\n type: '%s',\n clearable: true,\n shortcuts: %s,\n onUpdateValue: (e: any) => {\n console.log(e);\n },\n },\n },\n", field.TsName, "NDatePicker", field.Dc, "date", "defShortcuts()")
+ in.options.Step.ImportModel.UtilsDate = append(in.options.Step.ImportModel.UtilsDate, "defShortcuts")
+
+ case FormModeDateRange:
+ component = fmt.Sprintf(" {\n field: '%s',\n component: '%s',\n label: '%s',\n componentProps: {\n type: '%s',\n clearable: true,\n shortcuts: %s,\n onUpdateValue: (e: any) => {\n console.log(e);\n },\n },\n },\n", field.TsName, "NDatePicker", field.Dc, "daterange", "defRangeShortcuts()")
+ in.options.Step.ImportModel.UtilsDate = append(in.options.Step.ImportModel.UtilsDate, "defRangeShortcuts")
+
+ case FormModeTime:
+ component = fmt.Sprintf(" {\n field: '%s',\n component: '%s',\n label: '%s',\n componentProps: {\n type: '%s',\n clearable: true,\n shortcuts: %s,\n onUpdateValue: (e: any) => {\n console.log(e);\n },\n },\n },\n", field.TsName, "NDatePicker", field.Dc, "datetime", "defShortcuts()")
+ in.options.Step.ImportModel.UtilsDate = append(in.options.Step.ImportModel.UtilsDate, "defShortcuts")
+
+ case FormModeTimeRange:
+ component = fmt.Sprintf(" {\n field: '%s',\n component: '%s',\n label: '%s',\n componentProps: {\n type: '%s',\n clearable: true,\n shortcuts: %s,\n onUpdateValue: (e: any) => {\n console.log(e);\n },\n },\n },\n", field.TsName, "NDatePicker", field.Dc, "datetimerange", "defRangeShortcuts()")
+ in.options.Step.ImportModel.UtilsDate = append(in.options.Step.ImportModel.UtilsDate, "defRangeShortcuts")
+
+ case FormModeSwitch:
+ fallthrough
+ case FormModeRadio:
+ component = fmt.Sprintf(" {\n field: '%s',\n component: '%s',\n label: '%s',\n giProps: {\n //span: 24,\n },\n componentProps: {\n options: %v,\n onUpdateChecked: (e: any) => {\n console.log(e);\n },\n },\n },\n", field.TsName, "NRadioGroup", field.Dc, l.generateWebDictOption(in.options.dictMap[field.TsName]))
+
+ case FormModeCheckbox:
+ component = fmt.Sprintf(" {\n field: '%s',\n component: '%s',\n label: '%s',\n giProps: {\n span: 1,\n },\n componentProps: {\n placeholder: '请选择%s',\n options: %v,\n onUpdateChecked: (e: any) => {\n console.log(e);\n },\n },\n },\n", field.TsName, "NCheckbox", field.Dc, field.Dc, l.generateWebDictOption(in.options.dictMap[field.TsName]))
+
+ case FormModeSelect:
+ component = fmt.Sprintf(" {\n field: '%s',\n component: '%s',\n label: '%s',\n defaultValue: null,\n componentProps: {\n placeholder: '请选择%s',\n options: %v,\n onUpdateValue: (e: any) => {\n console.log(e);\n },\n },\n },\n", field.TsName, "NSelect", field.Dc, field.Dc, l.generateWebDictOption(in.options.dictMap[field.TsName]))
+
+ case FormModeSelectMultiple:
+ component = fmt.Sprintf(" {\n field: '%s',\n component: '%s',\n label: '%s',\n defaultValue: null,\n componentProps: {\n multiple: true,\n placeholder: '请选择%s',\n options: %v,\n onUpdateValue: (e: any) => {\n console.log(e);\n },\n },\n },\n", field.TsName, "NSelect", field.Dc, field.Dc, l.generateWebDictOption(in.options.dictMap[field.TsName]))
+
+ default:
+ component = defaultComponent
+ }
+
+ buffer.WriteString(component)
+ }
+}
+
+func (l *gCurd) generateWebModelColumns(ctx context.Context, in *CurdPreviewInput) (string, error) {
+ buffer := bytes.NewBuffer(nil)
+ buffer.WriteString("export const columns = [\n")
+
+ // 主表
+ if err := l.generateWebModelColumnsEach(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.generateWebModelColumnsEach(buffer, in, v.Columns); err != nil {
+ return "", err
+ }
+ }
+ }
+
+ buffer.WriteString("];\n")
+ return buffer.String(), nil
+}
+
+func (l *gCurd) generateWebModelColumnsEach(buffer *bytes.Buffer, in *CurdPreviewInput, fields []*sysin.GenCodesColumnListModel) (err error) {
+ for _, field := range fields {
+ if !field.IsList {
+ continue
+ }
+ var (
+ defaultComponent = fmt.Sprintf(" {\n title: '%s',\n key: '%s',\n align: '%v',\n width: %v,\n },\n", field.Dc, field.TsName, field.Align, field.Width)
+ component string
+ )
+
+ // 查询用户摘要
+ if in.options.Step.HasHookMemberSummary && IsMemberSummaryField(field.Name) {
+ buffer.WriteString(fmt.Sprintf(" {\n title: '%v',\n key: '%v',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderPopoverMemberSumma(row.%vSumma);\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", "MemberSumma"}...)
+ continue
+ }
+
+ // 这里根据编辑表单组件来进行推断,如果没有则使用默认input,这可能会导致和查询条件所需参数不符的情况
+ switch field.FormMode {
+ case FormModeDate:
+ component = fmt.Sprintf(" {\n title: '%s',\n key: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return formatToDate(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 FormModeRadio:
+ fallthrough
+ case FormModeSelect:
+ if g.IsEmpty(in.options.dictMap[field.TsName]) {
+ err = gerror.Newf("设置单选下拉框选项时,必须选择字典类型,字段名称:%v", field.Name)
+ return
+ }
+ component = fmt.Sprintf(" {\n title: '%s',\n key: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderOptionTag('%v', row.%v);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, in.options.dictMap[field.TsName], field.TsName)
+ in.options.Step.ImportModel.UtilsIndex = append(in.options.Step.ImportModel.UtilsIndex, "renderOptionTag")
+
+ case FormModeSelectMultiple:
+ if g.IsEmpty(in.options.dictMap[field.TsName]) {
+ err = gerror.Newf("设置多选下拉框选项时,必须选择字典类型,字段名称:%v", field.Name)
+ return
+ }
+ component = fmt.Sprintf(" {\n title: '%s',\n key: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n if (isNullObject(row.%s) || !Array.isArray(row.%s)) {\n return ``;\n }\n return row.%s.map((tagKey) => {\n return renderOptionTag('%s', 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 FormModeUploadImage:
+ component = fmt.Sprintf(" {\n title: '%s',\n key: '%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 FormModeUploadImages:
+ component = fmt.Sprintf(" {\n title: '%s',\n key: '%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 FormModeUploadFile:
+ component = fmt.Sprintf(" {\n title: '%s',\n key: '%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 FormModeUploadFiles:
+ component = fmt.Sprintf(" {\n title: '%s',\n key: '%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 FormModeSwitch:
+ component = fmt.Sprintf(" {\n title: '%s',\n key: '%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 : 2;\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, convert.CamelCaseToUnderline(field.TsName), field.TsName)
+ in.options.Step.ImportModel.NaiveUI = append(in.options.Step.ImportModel.NaiveUI, "NSwitch")
+
+ case FormModeRate:
+ component = fmt.Sprintf(" {\n title: '%s',\n key: '%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
+}
+
+// isStringType 判断字段是否为字符串类型
+func isStringType(tsType, dataType string) bool {
+ // 根据 TypeScript 类型判断
+ if tsType == "string" {
+ return true
+ }
+
+ // 根据数据库数据类型判断
+ stringTypes := []string{
+ // mysql
+ "varchar", "char", "text", "longtext", "mediumtext", "tinytext",
+ "nvarchar", "nchar", "ntext",
+ "string", "enum", "set",
+ // pgsql
+ "bpchar", "date", "time", "timetz", "timestamp", "timestamptz", "interval",
+ "uuid", "bytea", "inet", "cidr", "macaddr", "macaddr8", "bit", "varbit", "json", "jsonb",
+ "point", "line", "lseg", "box", "path", "polygon", "circle", "money",
+ }
+
+ for _, t := range stringTypes {
+ if strings.EqualFold(dataType, t) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/server/internal/library/hggen/views/curd_generate_web_view.go b/server/internal/library/hggen/views/curd_generate_web_view.go
new file mode 100644
index 0000000..dc932fd
--- /dev/null
+++ b/server/internal/library/hggen/views/curd_generate_web_view.go
@@ -0,0 +1,90 @@
+// Package views
+// @Link https://github.com/bufanyun/hotgo
+// @Copyright Copyright (c) 2023 HotGo CLI
+// @Author Ms <133814250@qq.com>
+// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
+package views
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+
+ "github.com/gogf/gf/v2/frame/g"
+)
+
+func (l *gCurd) webViewTplData(ctx context.Context, in *CurdPreviewInput) (data g.Map, err error) {
+ data = make(g.Map)
+ item, hasUpload := l.generateWebViewItem(ctx, in)
+ data["item"] = item
+ data["hasUploadFile"] = hasUpload
+ return
+}
+
+func (l *gCurd) generateWebViewItem(ctx context.Context, in *CurdPreviewInput) (string, bool) {
+ buffer := bytes.NewBuffer(nil)
+ hasUploadFile := false
+
+ for _, field := range in.masterFields {
+ if !field.IsEdit {
+ continue
+ }
+
+ // 检测是否使用了文件上传相关的组件
+ if field.FormMode == FormModeUploadFile || field.FormMode == FormModeUploadFiles {
+ hasUploadFile = true
+ }
+
+ var (
+ defaultComponent = fmt.Sprintf("\n %s \n {{ formValue.%s }}\n ", field.Dc, field.TsName)
+ component string
+ )
+
+ switch field.FormMode {
+ case FormModeInputTextarea, FormModeInputEditor:
+ component = fmt.Sprintf("\n %s \n \n ", field.Dc, field.TsName)
+
+ case FormModeInputYaml:
+ component = fmt.Sprintf("\n %s \n {{ formValue.%s }}\n ", field.Dc, field.TsName)
+
+ case FormModeInputDynamic:
+ component = defaultComponent
+
+ case FormModeDate:
+ component = defaultComponent
+
+ case FormModeTime:
+ component = defaultComponent
+
+ case FormModeRadio, FormModeSelect:
+ component = fmt.Sprintf("\n \n {{ dict.getLabel('%s', formValue.%s) }}\n \n ", field.Dc, in.options.dictMap[field.TsName], field.TsName, in.options.dictMap[field.TsName], field.TsName)
+
+ case FormModeCheckbox, FormModeSelectMultiple:
+ component = fmt.Sprintf("\n \n \n {{ dict.getLabel('%s', item) }}\n \n \n ", field.Dc, field.TsName, in.options.dictMap[field.TsName], in.options.dictMap[field.TsName])
+
+ case FormModeUploadImage:
+ component = fmt.Sprintf("\n %s \n \n ", field.Dc, field.TsName)
+
+ case FormModeUploadImages:
+ component = fmt.Sprintf("\n %s \n \n \n \n \n \n \n \n ", field.Dc, field.TsName)
+
+ case FormModeUploadFile:
+ component = fmt.Sprintf("\n %s \n \n
\n
\n
\n {{ getFileExt(formValue.%s) }}\n
\n
\n
\n
\n ", field.Dc, field.TsName, field.TsName, field.TsName)
+
+ case FormModeUploadFiles:
+ component = fmt.Sprintf("\n %s \n \n
\n \n
\n
\n \n {{ getFileExt(item) }}\n \n
\n
\n
\n \n
\n ", field.Dc, field.TsName)
+
+ case FormModeSwitch:
+ component = fmt.Sprintf("\n \n ", field.Dc, field.TsName)
+
+ case FormModeRate:
+ component = fmt.Sprintf("\n \n ", field.Dc, field.TsName)
+
+ default:
+ component = defaultComponent
+ }
+
+ buffer.WriteString(" " + component + "\n\n")
+ }
+ return buffer.String(), hasUploadFile
+}
diff --git a/server/internal/library/hggen/views/gohtml/consts.go b/server/internal/library/hggen/views/gohtml/consts.go
new file mode 100644
index 0000000..625118c
--- /dev/null
+++ b/server/internal/library/hggen/views/gohtml/consts.go
@@ -0,0 +1,7 @@
+package gohtml
+
+const (
+ defaultIndentString = " "
+ startIndent = 0
+ defaultLastElement = ""
+)
diff --git a/server/internal/library/hggen/views/gohtml/doc.go b/server/internal/library/hggen/views/gohtml/doc.go
new file mode 100644
index 0000000..f787ed3
--- /dev/null
+++ b/server/internal/library/hggen/views/gohtml/doc.go
@@ -0,0 +1,2 @@
+// Package gohtml provides an HTML formatting function.
+package gohtml
diff --git a/server/internal/library/hggen/views/gohtml/element.go b/server/internal/library/hggen/views/gohtml/element.go
new file mode 100644
index 0000000..a41be68
--- /dev/null
+++ b/server/internal/library/hggen/views/gohtml/element.go
@@ -0,0 +1,7 @@
+package gohtml
+
+// An element represents an HTML element.
+type element interface {
+ isInline() bool
+ write(*formattedBuffer, bool) bool
+}
diff --git a/server/internal/library/hggen/views/gohtml/formatter.go b/server/internal/library/hggen/views/gohtml/formatter.go
new file mode 100644
index 0000000..0f1e1f6
--- /dev/null
+++ b/server/internal/library/hggen/views/gohtml/formatter.go
@@ -0,0 +1,36 @@
+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()
+}
diff --git a/server/internal/library/hggen/views/gohtml/html_document.go b/server/internal/library/hggen/views/gohtml/html_document.go
new file mode 100644
index 0000000..3d9e6ea
--- /dev/null
+++ b/server/internal/library/hggen/views/gohtml/html_document.go
@@ -0,0 +1,54 @@
+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)
+}
diff --git a/server/internal/library/hggen/views/gohtml/parser.go b/server/internal/library/hggen/views/gohtml/parser.go
new file mode 100644
index 0000000..2ca7444
--- /dev/null
+++ b/server/internal/library/hggen/views/gohtml/parser.go
@@ -0,0 +1,128 @@
+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 and