效果图
1、默认状态

2、全屏状态

功能介绍
1、按键功能

2、细节介绍


-
最大化时支持实时预览json对象,同时支持错误提示和定位


- 点击中括号、花括号、大括号时会高亮显示另一半
- 支持括号、双引号自动补全
组件结构
JsonEditor.vue
| <template> |
| <div ref="center"> |
| <div |
| style=" |
| border: 1px solid #bac6e7; |
| padding-left: 5px; |
| padding-right: 5px; |
| display: flex; |
| justify-content: space-between; |
| " |
| > |
| <div style="display: flex; align-items: center"> |
| <eye-outlined class="icon_hover" @click="isPreview = true" title="预览JSON配置" /> |
| <div style="height: 18px; border: 1px solid #858585; margin: 0 3px"></div> |
| <tool-outlined class="icon_hover" @click="prettyFormat(viewJsonStr)" title="格式化" /> |
| <div style="height: 18px; border: 1px solid #858585; margin: 0 3px"></div> |
| <line-outlined |
| class="icon_hover" |
| @click="viewJsonStr = viewJsonStr.replace(/\s+/g, '')" |
| title="去除空格" |
| /> |
| <div |
| style=" |
| display: flex; |
| align-items: center; |
| border-left: 2px solid #858585; |
| height: 18px; |
| margin: 0 3px; |
| padding: 0 3px; |
| " |
| > |
| <fullscreen-outlined |
| v-if="!isFullScreen" |
| class="icon_hover" |
| @click="fullScreen" |
| title="全屏" |
| /> |
| <fullscreen-exit-outlined v-else class="icon_hover" @click="fullScreen" title="退出" /> |
| </div> |
| </div> |
| <div> |
| <check-circle-outlined title="格式正确" v-if="isPass" style="color: #63ca31" /> |
| <info-circle-outlined title="格式错误" v-else style="color: red" /> |
| </div> |
| </div> |
| <div class="edit-container"> |
| <textarea |
| wrap="off" |
| cols="1" |
| id="leftNum" |
| disabled |
| onscroll="document.getElementById('rightNum').scrollTop = this.scrollTop;" |
| ></textarea> |
| <a-textarea |
| ref="myTextarea" |
| id="rightNum" |
| :key="isFullScreen" |
| :auto-size="isFullScreen ? false : { minRows: rows, maxRows: rows }" |
| style="height: calc(100vh - 30px)" |
| placeholder="请输入JSON字符串" |
| onscroll="document.getElementById('leftNum').scrollTop = this.scrollTop;" |
| :value="viewJsonStr" |
| @click="handleClick" |
| @change="handleTextareaInput1" |
| /> |
| <vue-json-pretty |
| style="width: 50%; padding: 20px; background-color: white; border: 1px solid #d9d9d9" |
| v-if="isFullScreen" |
| :data="jsonObj" |
| /> |
| </div> |
| <a-modal |
| bodyStyle="padding:20px;border-radius: 5px" |
| title="预览JSON对象" |
| style="top: 112px; width: 800px" |
| :visible="isPreview" |
| @cancel="isPreview = false" |
| > |
| <div style="height: 500px; overflow: auto"> |
| <vue-json-pretty :data="jsonObj" /> |
| </div> |
| <template #footer> |
| <a-button @click="isPreview = false">关闭</a-button> |
| </template> |
| </a-modal> |
| </div> |
| </template> |
| |
| <script lang="ts" setup> |
| import VueJsonPretty from 'vue-json-pretty'; |
| import 'vue-json-pretty/lib/styles.css'; |
| import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; |
| import { cloneDeep } from 'lodash-es'; |
| import { |
| handleBackspace, |
| handleClick, |
| handleClickEnter, |
| handleTabKey, |
| handleTextareaInput, |
| } from '/@/components/JsonEditor'; |
| |
| const emit = defineEmits(['update:jsonStr']); |
| |
| const props = defineProps({ |
| jsonStr: { |
| type: String, |
| default: '', |
| }, |
| rows: { |
| type: Number, |
| default: 4, |
| }, |
| }); |
| const isPreview = ref(false); |
| const viewJsonStr: any = ref(props.jsonStr); |
| nextTick(() => { |
| viewJsonStr.value = props.jsonStr; |
| }); |
| |
| function handleTextareaInput1(event) { |
| handleTextareaInput(viewJsonStr, event); |
| } |
| |
| function IsJsonString(str) { |
| try { |
| JSON.parse(str); |
| } catch (e) { |
| return false; |
| } |
| return true; |
| } |
| |
| const isPass = ref(true); |
| watch( |
| () => viewJsonStr.value, |
| (newValue) => { |
| isPass.value = IsJsonString(newValue); |
| calculateNum(newValue); |
| emit('update:jsonStr', newValue); |
| }, |
| ); |
| |
| const num = ref(''); |
| |
| function calculateNum(value) { |
| if (value) { |
| let str = value; |
| str = str.replace(/\r/gi, ''); |
| str = str.split('\n'); |
| let n = str.length; |
| let lineBbj: any = document.getElementById('leftNum'); |
| for (let i = 1; i <= n; i++) { |
| if (document.all) { |
| num.value += i + '\r\n'; |
| } else { |
| num.value += i + '\n'; |
| } |
| } |
| lineBbj.value = num.value; |
| num.value = ''; |
| } |
| } |
| |
| const jsonObj = computed(() => { |
| const str = cloneDeep(props.jsonStr); |
| try { |
| return JSON.parse(str); |
| } catch (e: any) { |
| if (e.message?.match(/position\s+(\d+)/)) { |
| const location = e.message?.match(/position\s+(\d+)/)[1]; |
| const str1 = str.substring(0, location).trim(); |
| const str2 = str1.split('\n'); |
| const message = e.message.substring(0, e.message.indexOf('position')); |
| |
| if (str2[str2.length - 1]?.includes('[')) { |
| const { line, column } = getLineAndColumn(str1, str1.length - 1); |
| return `${message} at line ${line},column ${column}`; |
| } |
| const { line, column } = getLineAndColumn(str, location); |
| return `${message} at line ${line},column ${column}`; |
| } else { |
| return null; |
| } |
| } |
| }); |
| |
| |
| function getLineAndColumn(str, index) { |
| let line = 1; |
| let column = 1; |
| for (let i = 0; i < index; i++) { |
| if (str[i] === '\n') { |
| line++; |
| column = 1; |
| } else { |
| column++; |
| } |
| } |
| return { line, column }; |
| } |
| |
| |
| function prettyFormat(str) { |
| try { |
| |
| str = JSON.stringify(JSON.parse(str), null, 4); |
| str = str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); |
| viewJsonStr.value = str.replace( |
| /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, |
| function (match) { |
| return match; |
| }, |
| ); |
| } catch (e) { |
| console.log('异常信息:' + e); |
| } |
| } |
| |
| const center = ref(); |
| const isFullScreen = ref(false); |
| |
| function fullScreen() { |
| if (center.value) { |
| if (center.value.className.includes('fullScreen')) { |
| center.value.className = center.value.className.replace(' fullScreen', ''); |
| isFullScreen.value = false; |
| } else { |
| center.value.className += ' fullScreen'; |
| isFullScreen.value = true; |
| } |
| } |
| } |
| const myTextarea: any = ref(null); |
| function handleKeyDown(event) { |
| if (myTextarea.value) { |
| if (event.key === 'Backspace') { |
| handleBackspace(viewJsonStr, event); |
| } else if (event.key === 'Enter') { |
| handleClickEnter(viewJsonStr, event); |
| } else if (event.key === 'Tab') { |
| handleTabKey(event); |
| } |
| } |
| } |
| onMounted(() => { |
| window.addEventListener('keydown', handleKeyDown); |
| }); |
| onBeforeUnmount(() => { |
| window.removeEventListener('keydown', handleKeyDown); |
| }); |
| </script> |
| |
| <style scoped lang="less"> |
| .icon_hover { |
| &:hover { |
| color: #5c82ff; |
| } |
| } |
| |
| #leftNum { |
| overflow: hidden; |
| padding: 6px 2px; |
| width: 30px; |
| line-height: 22px; |
| font-size: 13px; |
| color: rgba(0, 0, 0, 0.25); |
| font-weight: bold; |
| resize: none; |
| text-align: center; |
| outline: none; |
| border: 0; |
| background: #f5f7fa; |
| box-sizing: border-box; |
| } |
| |
| #rightNum { |
| white-space: nowrap; |
| line-height: 22px; |
| |
| &::-webkit-scrollbar { |
| width: 5px; |
| height: 5px; |
| background-color: #efeae6; |
| } |
| } |
| |
| .leftBox { |
| height: 100%; |
| text-align: left; |
| } |
| |
| .edit-container { |
| border: 1px solid #f5f7fa; |
| display: flex; |
| background-color: #f5f7fa; |
| } |
| |
| .fullScreen { |
| position: fixed; |
| z-index: 9999; |
| height: 100vh; |
| width: 100vw; |
| left: 0; |
| top: 0; |
| right: 0; |
| bottom: 0; |
| background-color: #f5f7fa; |
| } |
| </style> |
复制
index.js
| import { nextTick } from 'vue'; |
| |
| export const handleTextareaInput = (viewJsonStr, event) => { |
| const textarea = event.target; |
| const cursorPosition: any = textarea.selectionStart; |
| const previousValue = viewJsonStr.value; |
| const newValue = textarea.value; |
| |
| if (newValue.length < previousValue.length) { |
| viewJsonStr.value = newValue; |
| |
| } else if (newValue.length > previousValue.length) { |
| viewJsonStr.value = newValue; |
| const value = textarea.value; |
| |
| if (value[cursorPosition - 1] === '"' && value[cursorPosition] !== '"') { |
| textarea.value = value.slice(0, cursorPosition) + '"' + value.slice(cursorPosition); |
| viewJsonStr.value = textarea.value; |
| textarea.selectionStart = cursorPosition; |
| textarea.selectionEnd = cursorPosition; |
| }else if (value[cursorPosition - 1] === "'" && value[cursorPosition] !== "'") { |
| textarea.value = value.slice(0, cursorPosition) + "'" + value.slice(cursorPosition); |
| viewJsonStr.value = textarea.value; |
| textarea.selectionStart = cursorPosition; |
| textarea.selectionEnd = cursorPosition; |
| } else if (value[cursorPosition - 1] === '{' && value[cursorPosition] !== '{') { |
| textarea.value = value.slice(0, cursorPosition) + '}' + value.slice(cursorPosition); |
| viewJsonStr.value = textarea.value; |
| textarea.selectionStart = cursorPosition; |
| textarea.selectionEnd = cursorPosition; |
| } else if (value[cursorPosition - 1] === '[' && value[cursorPosition] !== ']') { |
| textarea.value = value.slice(0, cursorPosition) + ']' + value.slice(cursorPosition); |
| viewJsonStr.value = textarea.value; |
| textarea.selectionStart = cursorPosition; |
| textarea.selectionEnd = cursorPosition; |
| } else if (value[cursorPosition - 1] === '(' && value[cursorPosition] !== ')') { |
| textarea.value = value.slice(0, cursorPosition) + ')' + value.slice(cursorPosition); |
| viewJsonStr.value = textarea.value; |
| textarea.selectionStart = cursorPosition; |
| textarea.selectionEnd = cursorPosition; |
| } |
| |
| } else { |
| viewJsonStr.value = newValue; |
| } |
| }; |
| |
| const findOpeningBracketIndex = (text, startIndex, char) => { |
| const openingBrackets = { |
| ']': '[', |
| '}': '{', |
| ')': '(', |
| }; |
| let count = 0; |
| for (let i = startIndex; i >= 0; i--) { |
| if (text.charAt(i) === char) { |
| count++; |
| } else if (text.charAt(i) === openingBrackets[char]) { |
| count--; |
| if (count === 0) { |
| return i; |
| } |
| } |
| } |
| return -1; |
| }; |
| |
| const findClosingBracketIndex = (text, startIndex, char) => { |
| const closingBrackets = { |
| '[': ']', |
| '{': '}', |
| '(': ')', |
| }; |
| let count = 0; |
| for (let i = startIndex; i < text.length; i++) { |
| if (text.charAt(i) === char) { |
| count++; |
| } else if (text.charAt(i) === closingBrackets[char]) { |
| count--; |
| if (count === 0) { |
| return i; |
| } |
| } |
| } |
| return -1; |
| }; |
| const isBracket = (char) => { |
| return ['[', ']', '{', '}', '(', ')'].includes(char); |
| }; |
| |
| export const handleClick = (event) => { |
| const textarea: any = document.getElementById('rightNum'); |
| const { selectionStart, selectionEnd, value } = textarea; |
| const clickedChar = value.charAt(selectionStart); |
| if (isBracket(clickedChar)) { |
| const openingBracketIndex = findOpeningBracketIndex(value, selectionStart, clickedChar); |
| const closingBracketIndex = findClosingBracketIndex(value, selectionStart, clickedChar); |
| if (openingBracketIndex !== -1) { |
| textarea.setSelectionRange(openingBracketIndex, openingBracketIndex + 1); |
| } else if (closingBracketIndex !== -1) { |
| textarea.setSelectionRange(closingBracketIndex, closingBracketIndex + 1); |
| } |
| } |
| }; |
| |
| export function handleClickEnter(viewJsonStr, event) { |
| if (event.key == 'Enter') { |
| const textarea = event.target; |
| const cursorPosition: any = textarea.selectionStart; |
| const value = textarea.value; |
| if ( |
| (value[cursorPosition - 1] === '{' && value[cursorPosition] == '}') || |
| (value[cursorPosition - 1] === '[' && value[cursorPosition] == ']') |
| ) { |
| textarea.value = value.slice(0, cursorPosition) + '\n' + value.slice(cursorPosition); |
| textarea.setSelectionRange(cursorPosition, cursorPosition); |
| viewJsonStr.value = textarea.value; |
| |
| setTimeout(() => { |
| handleTabKey(syntheticEvent); |
| }, 30); |
| } |
| } |
| } |
| |
| const syntheticEvent = new KeyboardEvent('keydown', { |
| key: 'Tab', |
| }); |
| |
| export const handleTabKey = (event) => { |
| const textarea: any = document.getElementById('rightNum'); |
| const { selectionStart, selectionEnd } = textarea; |
| const tabSpaces = ' '; |
| event.preventDefault(); |
| |
| textarea.value = |
| textarea.value.substring(0, selectionStart) + |
| tabSpaces + |
| textarea.value.substring(selectionEnd); |
| |
| textarea.selectionStart = selectionStart + tabSpaces.length; |
| textarea.selectionEnd = selectionStart + tabSpaces.length; |
| }; |
| |
| |
| export function handleBackspace(viewJsonStr, event) { |
| const textarea = event.target; |
| const cursorPosition = textarea.selectionStart; |
| const textBeforeCursor = viewJsonStr.value.slice(0, cursorPosition); |
| const textAfterCursor = viewJsonStr.value.slice(cursorPosition); |
| if ( |
| (textBeforeCursor.endsWith('"') && textAfterCursor.startsWith('"')) || |
| (textBeforeCursor.endsWith("'") && textAfterCursor.startsWith("'")) || |
| (textBeforeCursor.endsWith('[') && textAfterCursor.startsWith(']')) || |
| (textBeforeCursor.endsWith('{') && textAfterCursor.startsWith('}')) || |
| (textBeforeCursor.endsWith('(') && textAfterCursor.startsWith(')')) |
| ) { |
| event.preventDefault(); |
| viewJsonStr.value = textBeforeCursor.slice(0, -1) + textAfterCursor.slice(1); |
| nextTick(() => { |
| textarea.selectionStart = cursorPosition - 1; |
| textarea.selectionEnd = cursorPosition - 1; |
| }).then((r) => {}); |
| } |
| } |
复制
调用方法
| <json-editor v-model:json-str="formData.sessionToolConfig" :rows="8" /> |
复制
传参
v-model:json-str:双向绑定编辑器的值和使用的变量
rows:编辑器的行数