添加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

@@ -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",

277
web/pnpm-lock.yaml generated
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();