添加yaml字段类型

This commit is contained in:
wanna 2025-09-11 13:34:59 +08:00
parent 1688aaf371
commit a98a25ea12
13 changed files with 844 additions and 9 deletions

View File

@ -100,6 +100,7 @@ const (
FormModeInputNumber = "InputNumber" // 数字输入
FormModeInputTextarea = "InputTextarea" // 文本域
FormModeInputEditor = "InputEditor" // 富文本
FormModeInputYaml = "InputYaml" // YAML配置
FormModeInputDynamic = "InputDynamic" // 动态键值对
FormModeDate = "Date" // 日期选择(Y-M-D)
FormModeDateRange = "DateRange" // 日期范围选择
@ -122,7 +123,7 @@ const (
)
var FormModes = []string{
FormModeInput, FormModeInputNumber, FormModeInputTextarea, FormModeInputEditor, FormModeInputDynamic,
FormModeInput, FormModeInputNumber, FormModeInputTextarea, FormModeInputEditor, FormModeInputYaml, FormModeInputDynamic,
FormModeDate, FormModeDateRange, FormModeTime, FormModeTimeRange,
FormModeRadio, FormModeCheckbox, FormModeSelect, FormModeSelectMultiple, FormModeTreeSelect, FormModeCascader,
FormModeUploadImage, FormModeUploadImages, FormModeUploadFile, FormModeUploadFiles,
@ -136,6 +137,7 @@ var FormModeMap = map[string]string{
FormModeInputNumber: "数字输入",
FormModeInputTextarea: "文本域",
FormModeInputEditor: "富文本",
FormModeInputYaml: "YAML配置",
FormModeInputDynamic: "动态键值对",
FormModeDate: "日期选择(Y-M-D)",
FormModeDateRange: "日期范围选择",
@ -173,6 +175,7 @@ const (
FormRoleAccount = "account"
FormRolePassword = "password"
FormRoleAmount = "amount"
FormRoleYaml = "yaml"
)
var FormRoleMap = map[string]string{
@ -191,6 +194,7 @@ var FormRoleMap = map[string]string{
FormRoleAccount: "账号",
FormRolePassword: "密码",
FormRoleAmount: "金额",
FormRoleYaml: "YAML格式",
}
// 查询条件

View File

@ -30,6 +30,7 @@ const (
InputTypeInsertFields = 6 // 编辑新增过滤字段
InputTypeTreeOptionFields = 7 // 关系树查询字段
EditInpValidatorGenerally = "if err := g.Validator().Rules(\"%s\").Data(in.%s).Messages(\"%s\").Run(ctx); err != nil {\n\t\treturn err.Current()\n\t}\n"
EditInpValidatorYaml = "if err := validate.ValidateYAML(in.%s); err != nil {\n\t\treturn gerror.Newf(\"%s必须为有效的YAML格式: %%s\", err.Error())\n\t}\n"
)
func (l *gCurd) inputTplData(ctx context.Context, in *CurdPreviewInput) (data g.Map, err error) {
@ -308,6 +309,8 @@ func makeValidatorFunc(field *sysin.GenCodesColumnListModel) (err error, rule st
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 == FormRoleAmount {
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 if field.FormRole == FormRoleYaml {
rule = fmt.Sprintf(EditInpValidatorYaml, field.GoName, field.Dc)
} else {
err = gerror.New("not support")
}

View File

@ -99,10 +99,11 @@ func (l *gCurd) generateLogicSwitchFields(ctx context.Context, in *CurdPreviewIn
func (l *gCurd) generateLogicEdit(ctx context.Context, in *CurdPreviewInput) g.Map {
var (
data = make(g.Map)
updateBuffer = bytes.NewBuffer(nil)
insertBuffer = bytes.NewBuffer(nil)
uniqueBuffer = bytes.NewBuffer(nil)
data = make(g.Map)
updateBuffer = bytes.NewBuffer(nil)
insertBuffer = bytes.NewBuffer(nil)
uniqueBuffer = bytes.NewBuffer(nil)
validationBuffer = bytes.NewBuffer(nil)
)
for _, field := range in.masterFields {
@ -117,6 +118,11 @@ func (l *gCurd) generateLogicEdit(ctx context.Context, in *CurdPreviewInput) g.M
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))
}
// 添加 YAML 格式验证
if field.IsEdit && field.FormRole == FormRoleYaml {
validationBuffer.WriteString(fmt.Sprintf(EditInpValidatorYaml, field.GoName, field.Dc))
}
}
notFilterAuth := ""
@ -130,6 +136,7 @@ func (l *gCurd) generateLogicEdit(ctx context.Context, in *CurdPreviewInput) g.M
data["update"] = updateBuffer.String()
data["insert"] = insertBuffer.String()
data["unique"] = uniqueBuffer.String()
data["validation"] = validationBuffer.String()
return data
}

View File

@ -53,6 +53,9 @@ func (l *gCurd) generateWebEditFormItem(ctx context.Context, in *CurdPreviewInpu
case FormModeInputEditor:
component = fmt.Sprintf("<n-form-item label=\"%s\" path=\"%s\">\n <Editor style=\"height: 450px\" id=\"%s\" v-model:value=\"formValue.%s\" />\n </n-form-item>", field.Dc, field.TsName, field.TsName, field.TsName)
case FormModeInputYaml:
component = fmt.Sprintf("<n-form-item label=\"%s\" path=\"%s\">\n <YamlEditor ref=\"%sYamlRef\" v-model:value=\"formValue.%s\" :height=\"400\" placeholder=\"请输入%s\" validate-on-blur show-stats prevent-invalid-submit @error=\"handleYamlError\" @valid=\"handleYamlValid\" @validation-change=\"(isValid) => handleYamlValidationChange(isValid, '%s')\" />\n </n-form-item>", field.Dc, field.TsName, field.TsName, field.TsName, field.Dc, field.TsName)
case FormModeInputDynamic:
component = fmt.Sprintf("<n-form-item label=\"%s\" path=\"%s\">\n <n-dynamic-input\n v-model:value=\"formValue.%s\"\n preset=\"pair\"\n key-placeholder=\"键名\"\n value-placeholder=\"键值\"\n />\n </n-form-item>", field.Dc, field.TsName, field.TsName)
@ -150,6 +153,7 @@ func (l *gCurd) generateWebEditScript(ctx context.Context, in *CurdPreviewInput)
data = make(g.Map)
importBuffer = bytes.NewBuffer(nil)
setupBuffer = bytes.NewBuffer(nil)
hasYamlField = false
)
importBuffer.WriteString(" import { ref, computed } from 'vue';\n")
@ -190,6 +194,11 @@ func (l *gCurd) generateWebEditScript(ctx context.Context, in *CurdPreviewInput)
if !gstr.Contains(importBuffer.String(), `import Editor`) {
importBuffer.WriteString(" import Editor from '@/components/Editor/editor.vue';\n")
}
case FormModeInputYaml:
if !gstr.Contains(importBuffer.String(), `import YamlEditor`) {
importBuffer.WriteString(" import YamlEditor from '@/components/YamlEditor/index.vue';\n")
}
hasYamlField = true
case FormModeUploadImage, FormModeUploadImages:
if !gstr.Contains(importBuffer.String(), `import UploadImage`) {
importBuffer.WriteString(" import UploadImage from '@/components/Upload/uploadImage.vue';\n")
@ -207,6 +216,13 @@ func (l *gCurd) generateWebEditScript(ctx context.Context, in *CurdPreviewInput)
}
}
// 根据是否有 YAML 字段添加相应的验证逻辑
if hasYamlField {
setupBuffer.WriteString(" const yamlValidationStates = ref(new Map());\n const isFormValid = computed(() => {\n for (const [fieldName, isValid] of yamlValidationStates.value) {\n if (!isValid) return false;\n }\n return true;\n });\n const handleYamlError = (error) => {\n console.error('YAML 验证错误:', error);\n };\n const handleYamlValid = (content) => {\n console.log('YAML 验证通过:', content);\n };\n const handleYamlValidationChange = (isValid, fieldName) => {\n yamlValidationStates.value.set(fieldName, isValid);\n };\n")
} else {
setupBuffer.WriteString(" const isFormValid = ref(true);\n")
}
data["import"] = importBuffer.String()
data["setup"] = setupBuffer.String()
return data

View File

@ -309,7 +309,7 @@ func (l *gCurd) generateWebModelFormSchemaEach(buffer *bytes.Buffer, fields []*s
// 这里根据编辑表单组件来进行推断如果没有则使用默认input这可能会导致和查询条件所需参数不符的情况
switch field.FormMode {
case FormModeInput, FormModeInputTextarea, FormModeInputEditor:
case FormModeInput, FormModeInputTextarea, FormModeInputEditor, FormModeInputYaml:
component = defaultComponent
case FormModeInputNumber:

View File

@ -34,6 +34,9 @@ func (l *gCurd) generateWebViewItem(ctx context.Context, in *CurdPreviewInput) s
case FormModeInputTextarea, FormModeInputEditor:
component = fmt.Sprintf("<n-descriptions-item>\n <template #label>%s</template>\n <span v-html=\"formValue.%s\"></span></n-descriptions-item>", field.Dc, field.TsName)
case FormModeInputYaml:
component = fmt.Sprintf("<n-descriptions-item>\n <template #label>%s</template>\n <pre style=\"white-space: pre-wrap; font-family: monospace; background: #f5f5f5; padding: 10px; border-radius: 4px;\">{{ formValue.%s }}</pre></n-descriptions-item>", field.Dc, field.TsName)
case FormModeInputDynamic:
component = defaultComponent

View File

@ -0,0 +1,50 @@
// Package validate
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2025 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package validate
import (
"strings"
"gopkg.in/yaml.v3"
)
// IsValidYAML 验证字符串是否为有效的YAML格式
func IsValidYAML(yamlStr string) bool {
if strings.TrimSpace(yamlStr) == "" {
return true // 空字符串被认为是有效的
}
var temp interface{}
err := yaml.Unmarshal([]byte(yamlStr), &temp)
return err == nil
}
// ValidateYAML 验证YAML格式并返回错误信息
func ValidateYAML(yamlStr string) error {
if strings.TrimSpace(yamlStr) == "" {
return nil // 空字符串被认为是有效的
}
var temp interface{}
err := yaml.Unmarshal([]byte(yamlStr), &temp)
return err
}
// ParseYAML 解析YAML字符串为interface{}
func ParseYAML(yamlStr string) (interface{}, error) {
var result interface{}
err := yaml.Unmarshal([]byte(yamlStr), &result)
return result, err
}
// ToYAML 将interface{}转换为YAML字符串
func ToYAML(data interface{}) (string, error) {
bytes, err := yaml.Marshal(data)
if err != nil {
return "", err
}
return string(bytes), nil
}

View File

@ -123,7 +123,7 @@ func (s *s@{.servFunName}) Export(ctx context.Context, in *@{.templateGroup}in.@
@{ if eq .options.Step.HasEdit true }
// Edit 修改/新增@{.tableComment}
func (s *s@{.servFunName}) Edit(ctx context.Context, in *@{.templateGroup}in.@{.varName}EditInp) (err error) {
@{.edit.unique} return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
@{.edit.unique}@{.edit.validation} return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
@{ if eq .options.Step.IsTreeTable true }in.Pid, in.Level, in.Tree, err = hgorm.AutoUpdateTree(ctx, &dao.@{.daoName}, in.@{.pk.GoName}, in.Pid)
if err != nil {
return err

View File

@ -30,7 +30,7 @@
<template #action>
<n-space>
<n-button @click="closeForm"> 取消 </n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm"> 确定 </n-button>
<n-button type="info" :loading="formBtnLoading" :disabled="!isFormValid" @click="confirmForm"> 确定 </n-button>
</n-space>
</template>
</n-modal>
@ -54,7 +54,7 @@
const dialogWidth = computed(() => {
return adaModalWidth(840);
});
@{.script.setup}
// 提交表单
function confirmForm(e) {
e.preventDefault();

View File

@ -29,6 +29,11 @@
"test prod gzip": "http-server dist --cors --gzip -c-1"
},
"dependencies": {
"@codemirror/basic-setup": "^0.20.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.2",
"@vicons/antd": "^0.12.0",
"@vicons/ionicons5": "^0.12.0",
"@vue/runtime-core": "^3.4.38",
@ -36,11 +41,13 @@
"@vueuse/core": "^11.0.3",
"axios": "^1.7.7",
"blueimp-md5": "^2.19.0",
"codemirror": "^6.0.2",
"date-fns": "^3.6.0",
"echarts": "^5.5.1",
"element-resize-detector": "^1.2.4",
"fingerprintjs2": "^2.1.4",
"highlight.js": "^11.10.0",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"mint-filter": "^4.0.3",
"mitt": "^3.0.1",
@ -68,6 +75,7 @@
"@commitlint/config-conventional": "^19.4.1",
"@eslint/eslintrc": "^3.1.0",
"@types/fs-extra": "^11.0.4",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.5.2",
"@typescript-eslint/eslint-plugin": "^8.4.0",

View File

@ -8,6 +8,21 @@ importers:
.:
dependencies:
'@codemirror/basic-setup':
specifier: ^0.20.0
version: 0.20.0
'@codemirror/lang-yaml':
specifier: ^6.1.2
version: 6.1.2
'@codemirror/state':
specifier: ^6.5.2
version: 6.5.2
'@codemirror/theme-one-dark':
specifier: ^6.1.3
version: 6.1.3
'@codemirror/view':
specifier: ^6.38.2
version: 6.38.2
'@vicons/antd':
specifier: ^0.12.0
version: 0.12.0
@ -29,6 +44,9 @@ importers:
blueimp-md5:
specifier: ^2.19.0
version: 2.19.0
codemirror:
specifier: ^6.0.2
version: 6.0.2
date-fns:
specifier: ^3.6.0
version: 3.6.0
@ -44,6 +62,9 @@ importers:
highlight.js:
specifier: ^11.10.0
version: 11.10.0
js-yaml:
specifier: ^4.1.0
version: 4.1.0
lodash-es:
specifier: ^4.17.21
version: 4.17.21
@ -120,6 +141,9 @@ importers:
'@types/fs-extra':
specifier: ^11.0.4
version: 11.0.4
'@types/js-yaml':
specifier: ^4.0.9
version: 4.0.9
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
@ -503,6 +527,58 @@ packages:
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
'@codemirror/autocomplete@0.20.3':
resolution: {integrity: sha512-lYB+NPGP+LEzAudkWhLfMxhTrxtLILGl938w+RcFrGdrIc54A+UgmCoz+McE3IYRFp4xyQcL4uFJwo+93YdgHw==}
'@codemirror/autocomplete@6.18.7':
resolution: {integrity: sha512-8EzdeIoWPJDsMBwz3zdzwXnUpCzMiCyz5/A3FIPpriaclFCGDkAzK13sMcnsu5rowqiyeQN2Vs2TsOcoDPZirQ==}
'@codemirror/basic-setup@0.20.0':
resolution: {integrity: sha512-W/ERKMLErWkrVLyP5I8Yh8PXl4r+WFNkdYVSzkXYPQv2RMPSkWpr2BgggiSJ8AHF/q3GuApncDD8I4BZz65fyg==}
deprecated: In version 6.0, this package has been renamed to just 'codemirror'
'@codemirror/commands@0.20.0':
resolution: {integrity: sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==}
'@codemirror/commands@6.8.1':
resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==}
'@codemirror/lang-yaml@6.1.2':
resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==}
'@codemirror/language@0.20.2':
resolution: {integrity: sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==}
'@codemirror/language@6.11.3':
resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==}
'@codemirror/lint@0.20.3':
resolution: {integrity: sha512-06xUScbbspZ8mKoODQCEx6hz1bjaq9m8W8DxdycWARMiiX1wMtfCh/MoHpaL7ws/KUMwlsFFfp2qhm32oaCvVA==}
'@codemirror/lint@6.8.5':
resolution: {integrity: sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==}
'@codemirror/search@0.20.1':
resolution: {integrity: sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==}
'@codemirror/search@6.5.11':
resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==}
'@codemirror/state@0.20.1':
resolution: {integrity: sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==}
'@codemirror/state@6.5.2':
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
'@codemirror/theme-one-dark@6.1.3':
resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
'@codemirror/view@0.20.7':
resolution: {integrity: sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==}
'@codemirror/view@6.38.2':
resolution: {integrity: sha512-bTWAJxL6EOFLPzTx+O5P5xAO3gTqpatQ2b/ARQ8itfU/v2LlpS3pH2fkL0A3E/Fx8Y2St2KES7ZEV0sHTsSW/A==}
'@commitlint/cli@19.4.1':
resolution: {integrity: sha512-EerFVII3ZcnhXsDT9VePyIdCJoh3jEzygN1L37MjQXgPfGS6fJTWL/KHClVMod1d8w94lFC3l4Vh/y5ysVAz2A==}
engines: {node: '>=v18'}
@ -1036,6 +1112,30 @@ packages:
'@juggle/resize-observer@3.4.0':
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
'@lezer/common@0.16.1':
resolution: {integrity: sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==}
'@lezer/common@1.2.3':
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
'@lezer/highlight@0.16.0':
resolution: {integrity: sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==}
'@lezer/highlight@1.2.1':
resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==}
'@lezer/lr@0.16.3':
resolution: {integrity: sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==}
'@lezer/lr@1.4.2':
resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==}
'@lezer/yaml@1.0.3':
resolution: {integrity: sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==}
'@marijn/find-cluster-break@1.0.2':
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -1287,6 +1387,9 @@ packages:
'@types/istanbul-reports@3.0.4':
resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
'@types/js-yaml@4.0.9':
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@ -1899,6 +2002,9 @@ packages:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
codemirror@6.0.2:
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
collect-v8-coverage@1.0.2:
resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==}
@ -2028,6 +2134,9 @@ packages:
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
@ -4138,6 +4247,9 @@ packages:
resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==}
engines: {node: '>=0.10.0'}
style-mod@4.1.2:
resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==}
stylelint-config-prettier@9.0.5:
resolution: {integrity: sha512-U44lELgLZhbAD/xy/vncZ2Pq8sh2TnpiPvo38Ifg9+zeioR+LAkHu0i6YORIOxFafZoVg0xqQwex6e6F25S5XA==}
engines: {node: '>= 12'}
@ -4555,6 +4667,9 @@ packages:
peerDependencies:
vue: ^3.0.11
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
@ -4931,6 +5046,122 @@ snapshots:
'@bcoe/v8-coverage@0.2.3': {}
'@codemirror/autocomplete@0.20.3':
dependencies:
'@codemirror/language': 0.20.2
'@codemirror/state': 0.20.1
'@codemirror/view': 0.20.7
'@lezer/common': 0.16.1
'@codemirror/autocomplete@6.18.7':
dependencies:
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.2
'@lezer/common': 1.2.3
'@codemirror/basic-setup@0.20.0':
dependencies:
'@codemirror/autocomplete': 0.20.3
'@codemirror/commands': 0.20.0
'@codemirror/language': 0.20.2
'@codemirror/lint': 0.20.3
'@codemirror/search': 0.20.1
'@codemirror/state': 0.20.1
'@codemirror/view': 0.20.7
'@codemirror/commands@0.20.0':
dependencies:
'@codemirror/language': 0.20.2
'@codemirror/state': 0.20.1
'@codemirror/view': 0.20.7
'@lezer/common': 0.16.1
'@codemirror/commands@6.8.1':
dependencies:
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.2
'@lezer/common': 1.2.3
'@codemirror/lang-yaml@6.1.2':
dependencies:
'@codemirror/autocomplete': 6.18.7
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
'@lezer/yaml': 1.0.3
'@codemirror/language@0.20.2':
dependencies:
'@codemirror/state': 0.20.1
'@codemirror/view': 0.20.7
'@lezer/common': 0.16.1
'@lezer/highlight': 0.16.0
'@lezer/lr': 0.16.3
style-mod: 4.1.2
'@codemirror/language@6.11.3':
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.2
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
style-mod: 4.1.2
'@codemirror/lint@0.20.3':
dependencies:
'@codemirror/state': 0.20.1
'@codemirror/view': 0.20.7
crelt: 1.0.6
'@codemirror/lint@6.8.5':
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.2
crelt: 1.0.6
'@codemirror/search@0.20.1':
dependencies:
'@codemirror/state': 0.20.1
'@codemirror/view': 0.20.7
crelt: 1.0.6
'@codemirror/search@6.5.11':
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.2
crelt: 1.0.6
'@codemirror/state@0.20.1': {}
'@codemirror/state@6.5.2':
dependencies:
'@marijn/find-cluster-break': 1.0.2
'@codemirror/theme-one-dark@6.1.3':
dependencies:
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.2
'@lezer/highlight': 1.2.1
'@codemirror/view@0.20.7':
dependencies:
'@codemirror/state': 0.20.1
style-mod: 4.1.2
w3c-keyname: 2.2.8
'@codemirror/view@6.38.2':
dependencies:
'@codemirror/state': 6.5.2
crelt: 1.0.6
style-mod: 4.1.2
w3c-keyname: 2.2.8
'@commitlint/cli@19.4.1(@types/node@22.5.2)(typescript@5.5.4)':
dependencies:
'@commitlint/format': 19.3.0
@ -5463,6 +5694,34 @@ snapshots:
'@juggle/resize-observer@3.4.0': {}
'@lezer/common@0.16.1': {}
'@lezer/common@1.2.3': {}
'@lezer/highlight@0.16.0':
dependencies:
'@lezer/common': 0.16.1
'@lezer/highlight@1.2.1':
dependencies:
'@lezer/common': 1.2.3
'@lezer/lr@0.16.3':
dependencies:
'@lezer/common': 0.16.1
'@lezer/lr@1.4.2':
dependencies:
'@lezer/common': 1.2.3
'@lezer/yaml@1.0.3':
dependencies:
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
'@marijn/find-cluster-break@1.0.2': {}
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -5665,6 +5924,8 @@ snapshots:
dependencies:
'@types/istanbul-lib-report': 3.0.3
'@types/js-yaml@4.0.9': {}
'@types/json-schema@7.0.15':
optional: true
@ -6451,6 +6712,16 @@ snapshots:
co@4.6.0: {}
codemirror@6.0.2:
dependencies:
'@codemirror/autocomplete': 6.18.7
'@codemirror/commands': 6.8.1
'@codemirror/language': 6.11.3
'@codemirror/lint': 6.8.5
'@codemirror/search': 6.5.11
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.2
collect-v8-coverage@1.0.2: {}
color-convert@1.9.3:
@ -6594,6 +6865,8 @@ snapshots:
create-require@1.1.1:
optional: true
crelt@1.0.6: {}
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.3
@ -8909,6 +9182,8 @@ snapshots:
dependencies:
escape-string-regexp: 1.0.5
style-mod@4.1.2: {}
stylelint-config-prettier@9.0.5(stylelint@16.9.0(typescript@5.5.4)):
dependencies:
stylelint: 16.9.0(typescript@5.5.4)
@ -9403,6 +9678,8 @@ snapshots:
vooks: 0.2.12(vue@3.4.38(typescript@5.5.4))
vue: 3.4.38(typescript@5.5.4)
w3c-keyname@2.2.8: {}
walker@1.0.8:
dependencies:
makeerror: 1.0.12

View File

@ -0,0 +1,445 @@
<template>
<div class="yaml-editor-container">
<div
ref="editorRef"
class="yaml-editor"
:class="{ 'error-state': hasError }"
/>
<div v-if="errorMessage" class="error-message">
<n-alert type="error" closable @close="clearError">
{{ errorMessage }}
</n-alert>
</div>
<div v-if="showStats" class="editor-stats">
<n-space size="small">
<n-tag size="small" type="info">
行数: {{ lineCount }}
</n-tag>
<n-tag size="small" type="success" v-if="!hasError">
语法正确
</n-tag>
<n-tag size="small" type="error" v-if="hasError">
语法错误
</n-tag>
</n-space>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, watch, nextTick, computed } from 'vue';
import { NAlert, NSpace, NTag, useMessage } from 'naive-ui';
import { EditorView, basicSetup } from 'codemirror';
import { EditorState } from '@codemirror/state';
import { yaml } from '@codemirror/lang-yaml';
import { oneDark } from '@codemirror/theme-one-dark';
import * as yamlParser from 'js-yaml';
export interface Props {
value?: string;
placeholder?: string;
height?: string | number;
disabled?: boolean;
readonly?: boolean;
darkTheme?: boolean;
showStats?: boolean;
validateOnChange?: boolean;
validateOnBlur?: boolean;
preventInvalidSubmit?: boolean;
}
const emit = defineEmits(['update:value', 'blur', 'focus', 'error', 'valid', 'change', 'validation-change']);
const message = useMessage();
const props = withDefaults(defineProps<Props>(), {
value: '',
placeholder: '# YAML 配置示例\n# 支持注释\nkey: value\nlist:\n - item1\n - item2\nconfig:\n enabled: true\n timeout: 30',
height: 300,
disabled: false,
readonly: false,
darkTheme: false,
showStats: true,
validateOnChange: false,
validateOnBlur: true,
preventInvalidSubmit: true,
});
const editorRef = ref<HTMLElement>();
const editorView = ref<EditorView>();
const errorMessage = ref('');
const hasError = ref(false);
const currentValue = ref(props.value || '');
//
const lineCount = computed(() => {
return currentValue.value.split('\n').length;
});
const isValid = computed(() => {
return !hasError.value || !currentValue.value.trim();
});
// value
watch(
() => props.value,
(newValue) => {
if (newValue !== currentValue.value && editorView.value) {
currentValue.value = newValue || '';
editorView.value.dispatch({
changes: {
from: 0,
to: editorView.value.state.doc.length,
insert: newValue || ''
}
});
clearError();
}
},
{ immediate: false }
);
// YAML
function validateYaml(content: string): boolean {
if (!content.trim()) {
clearError();
return true;
}
try {
yamlParser.load(content);
clearError();
return true;
} catch (error: any) {
const errorMsg = `YAML语法错误: ${error.message}`;
errorMessage.value = errorMsg;
hasError.value = true;
emit('error', { message: errorMsg, error });
return false;
}
}
function clearError() {
errorMessage.value = '';
hasError.value = false;
}
//
function onContentChange(content: string) {
currentValue.value = content;
// YAML
const valid = validateYaml(content);
//
emit('update:value', content);
emit('change', content);
//
emit('validation-change', valid);
if (props.validateOnChange && valid) {
emit('valid', content);
}
}
//
function addSelectionStyles() {
const styleId = 'yaml-editor-selection-styles';
let existingStyle = document.getElementById(styleId);
if (existingStyle) {
existingStyle.remove();
}
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
.yaml-editor .cm-selectionBackground {
background-color: rgba(64, 152, 252, 0.4) !important;
}
.yaml-editor .cm-focused .cm-selectionBackground {
background-color: rgba(64, 152, 252, 0.5) !important;
}
.yaml-editor .cm-selectionLayer .cm-selectionBackground {
background-color: rgba(64, 152, 252, 0.4) !important;
}
.yaml-editor .cm-content ::selection {
background-color: rgba(64, 152, 252, 0.4) !important;
}
.yaml-editor .cm-line::selection {
background-color: rgba(64, 152, 252, 0.4) !important;
}
`;
document.head.appendChild(style);
}
//
function createEditor() {
if (!editorRef.value) return;
//
addSelectionStyles();
const extensions = [
basicSetup,
yaml(),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const content = update.state.doc.toString();
onContentChange(content);
}
if (update.focusChanged) {
if (update.view.hasFocus) {
emit('focus');
} else {
emit('blur');
if (props.validateOnBlur) {
const content = update.state.doc.toString();
const isValid = validateYaml(content);
if (isValid) {
emit('valid', content);
}
}
}
}
}),
EditorView.theme({
'&': {
fontSize: '14px',
border: '1px solid #d9d9d9',
borderRadius: '6px',
},
'&.cm-focused': {
outline: 'none',
borderColor: '#4098fc',
boxShadow: '0 0 0 2px rgba(64, 152, 252, 0.2)',
},
'.cm-content': {
padding: '12px',
minHeight: typeof props.height === 'number' ? `${props.height}px` : props.height,
fontFamily: '"SF Mono", Monaco, Inconsolata, "Roboto Mono", Consolas, "Courier New", monospace',
lineHeight: '1.6',
},
'.cm-focused .cm-cursor': {
borderColor: '#4098fc',
},
'&.error-state': {
borderColor: '#ff4757',
},
'&.error-state.cm-focused': {
borderColor: '#ff4757',
boxShadow: '0 0 0 2px rgba(255, 71, 87, 0.2)',
}
}),
EditorState.readOnly.of(props.readonly),
];
//
if (props.darkTheme) {
extensions.push(oneDark);
}
const initialState = EditorState.create({
doc: currentValue.value || props.placeholder,
extensions,
});
editorView.value = new EditorView({
state: initialState,
parent: editorRef.value,
});
//
if (!currentValue.value && props.placeholder) {
nextTick(() => {
if (editorView.value) {
editorView.value.dispatch({
selection: { anchor: 0, head: editorView.value.state.doc.length }
});
}
});
}
}
//
function validate(): boolean {
return validateYaml(currentValue.value);
}
//
function getParsedData(): any {
try {
return yamlParser.load(currentValue.value);
} catch (error) {
return null;
}
}
//
function setData(data: any) {
try {
const yamlString = yamlParser.dump(data, {
indent: 2,
lineWidth: -1,
noRefs: true,
sortKeys: false,
quotingType: '"',
forceQuotes: false,
});
if (editorView.value) {
editorView.value.dispatch({
changes: {
from: 0,
to: editorView.value.state.doc.length,
insert: yamlString
}
});
}
currentValue.value = yamlString;
emit('update:value', yamlString);
clearError();
} catch (error: any) {
const errorMsg = `数据转换YAML失败: ${error.message}`;
errorMessage.value = errorMsg;
hasError.value = true;
emit('error', { message: errorMsg, error });
}
}
//
function getValue(): string {
return currentValue.value;
}
//
function focus() {
editorView.value?.focus();
}
//
function insertText(text: string) {
if (editorView.value) {
const selection = editorView.value.state.selection.main;
editorView.value.dispatch({
changes: {
from: selection.from,
to: selection.to,
insert: text
}
});
}
}
onMounted(() => {
nextTick(() => {
createEditor();
});
});
onBeforeUnmount(() => {
if (editorView.value) {
editorView.value.destroy();
}
//
const existingStyle = document.getElementById('yaml-editor-selection-styles');
if (existingStyle) {
existingStyle.remove();
}
});
//
defineExpose({
validate,
getParsedData,
setData,
getValue,
focus,
insertText,
clearError,
hasError: () => hasError.value,
isValid: () => isValid.value,
});
</script>
<style lang="less" scoped>
.yaml-editor-container {
width: 100%;
.yaml-editor {
border-radius: 6px;
transition: all 0.2s;
&.error-state {
border-color: #ff4757;
}
:deep(.cm-editor) {
outline: none;
}
:deep(.cm-content) {
background: #fafafa;
}
:deep(.cm-line) {
line-height: 1.6;
}
// YAML
:deep(.cm-comment) {
color: #8e8e93;
font-style: italic;
}
:deep(.cm-string) {
color: #0c7d9d;
}
:deep(.cm-number) {
color: #1c00cf;
}
:deep(.cm-keyword) {
color: #ad3da4;
font-weight: 600;
}
:deep(.cm-property) {
color: #3f6ec7;
font-weight: 500;
}
}
.error-message {
margin-top: 8px;
}
.editor-stats {
margin-top: 8px;
display: flex;
justify-content: flex-end;
}
}
//
.dark {
.yaml-editor-container {
.yaml-editor {
:deep(.cm-content) {
background: #1f1f1f;
color: #d4d4d4;
}
:deep(.cm-selectionBackground) {
background-color: rgba(64, 152, 252, 0.6) !important;
}
:deep(.cm-focused .cm-selectionBackground) {
background-color: rgba(64, 152, 252, 0.7) !important;
}
}
}
}
</style>

View File

@ -1,4 +1,5 @@
import { FormItemRule } from 'naive-ui';
import * as yaml from 'js-yaml';
/**
* @description
*/
@ -221,6 +222,27 @@ export const validate = {
}
return true;
},
// YAML格式验证
yaml(rule: FormItemRule, value: any, callback: Function): boolean | Error {
if (!value && !rule.required) {
callback();
return true;
}
if (!value) {
callback(new Error('请输入YAML配置'));
return false;
}
try {
// 使用 js-yaml 进行同步验证
yaml.load(value);
callback();
return true;
} catch (error: any) {
callback(new Error(`YAML格式错误: ${error.message}`));
return false;
}
},
// 不验证
none(_rule: FormItemRule, _value: any, callback: Function): boolean {
callback();