Vue實(shí)現(xiàn)JSON字符串格式化編輯器組件功能
相信很多同學(xué)都用過網(wǎng)上的在線JSON格式化工具來將雜亂的JSON數(shù)據(jù)轉(zhuǎn)換成易于我們閱讀和編輯的格式。那么,你有沒有想過自己動(dòng)手實(shí)現(xiàn)一個(gè)這樣的工具呢?今天,我將介紹如何使用Vue.js來構(gòu)建一個(gè)簡(jiǎn)單的JSON格式化工具。
功能簡(jiǎn)述
- 支持格式化JSON字符串
- 支持去除字符串中的空格
- 支持全屏操作
- 實(shí)時(shí)展示格式化狀態(tài)
- 控制臺(tái)展示成功和失敗的詳情,支持錯(cuò)誤定位
- 編輯器精準(zhǔn)計(jì)算字符串的行號(hào)
效果圖展示
默認(rèn)
全屏
功能介紹
按鈕
其他
1、自動(dòng)補(bǔ)全
輸入”(“、”{“、”[“將會(huì)自動(dòng)補(bǔ)全另一半
2、自動(dòng)刪除
刪除括號(hào)時(shí)也會(huì)自動(dòng)刪除另一半
3、括號(hào)匹配
點(diǎn)擊括號(hào)會(huì)高亮另一半括號(hào),方便定位
4、支持ctrl+z撤銷和ctrl+y重做功能
5、編輯器根據(jù)字符串的換行計(jì)算行號(hào)并展示
代碼
vue文件
<!--JsonEditor.vue--> <template> <div ref="center" id="editor_body" :style="{ height: editorHeight, width: editorWidth }"> <div style="height: 80%"> <div class="tool_slider"> <div style="display: flex; align-items: center"> <img src="@/assets/icons/format.svg" class="icon_hover" @click="prettyFormat(viewJsonStr)" title="格式化" /> <div style="height: 18px; border: 1px solid #858585; margin: 0 3px"></div> <img src="@/assets/icons/clearLine.svg" 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; " > <img src="@/assets/icons/full.svg" v-if="!isFullScreen" class="icon_hover" @click="fullScreen" title="全屏" /> <img src="@/assets/icons/closeFull.svg" v-else class="icon_hover" @click="fullScreen" title="退出" /> </div> </div> <div style="display: flex; align-items: center"> <img src="@/assets/icons/success.svg" title="格式正確" v-if="isPass" style="height: 20px; width: 20px" /> <img src="@/assets/icons/error.svg" title="格式錯(cuò)誤" v-else style="height: 17px; width: 17px" /> </div> </div> <div class="edit-container"> <textarea wrap="off" cols="1" id="leftNum" disabled onscroll="document.getElementById('rightNum').scrollTop = this.scrollTop;" ></textarea> <textarea ref="myTextarea" id="rightNum" :key="isFullScreen" style="width: 100%" placeholder="請(qǐng)輸入JSON字符串" onscroll="document.getElementById('leftNum').scrollTop = this.scrollTop;" :value="viewJsonStr" @click="handleClick" @input="handleTextareaInput1" /> </div> </div> <div id="console">{{ jsonObj }}</div> </div> </template> <script lang="ts" setup> import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { cloneDeep } from 'lodash-es'; import { handleBackspace, handleClick, handleClickEnter, handleTabKey, handleTextareaInput, setAutoKey, } from '@/components/JsonEditor'; const emit = defineEmits(['update:value']); const props = defineProps({ value: { type: String, default: '', }, width: { type: String, default: '1000px', }, height: { type: String, default: '400px', }, }); const viewJsonStr: any = ref(props.value); const editorWidth: any = ref(JSON.parse(JSON.stringify(props.width))); const editorHeight: any = ref(JSON.parse(JSON.stringify(props.height))); // 自動(dòng)補(bǔ)全 function handleTextareaInput1(event) { handleTextareaInput(viewJsonStr, event); } const isPass = ref(true); watch( () => viewJsonStr.value, (newValue) => { calculateNum(newValue); emit('update:value', newValue); }, ); const num = ref(''); function calculateNum(value) { let lineBbj: any = document.getElementById('leftNum'); num.value = ''; let str = value; if (str === null || str === undefined) { str = ''; } str = str.replace(/\r/gi, ''); str = str.split('\n'); let n = str.length; if (n.toString().length > 3) { lineBbj.style.width = n.toString().length * 10 + 'px'; } else { lineBbj.style.width = '30px'; } for (let i = 1; i <= n; i++) { if (document.all) { num.value += i + '\r\n'; //判斷瀏覽器是否是IE } else { num.value += i + '\n'; } } lineBbj.value = num.value; } // 預(yù)覽對(duì)象 const jsonObj = computed(() => { const str = cloneDeep(viewJsonStr.value); // 如果輸入的全是數(shù)字,JSON.parse(str)不會(huì)報(bào)錯(cuò),需要手動(dòng)處理一下 const onlyNumber = /^\d+$/.test(str); const dom = document.getElementById('console'); function setColor(color) { if (dom) { dom.style.color = color; } } if (str) { try { if (onlyNumber) { setColor('red'); isPass.value = false; return getCurrentTime() + str + ' is not valid JSON'; } setColor('black'); isPass.value = true; if (JSON.parse(str)) { setColor('green'); return `${getCurrentTime()}校驗(yàn)通過`; } } catch (e: any) { isPass.value = false; setColor('red'); 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')); // 如果當(dāng)前行或者前一行有'[' 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 `${getCurrentTime()}${message} at line ${line},column ${column}`; } else { return getCurrentTime() + str + ' is not valid JSON'; } } } else { return null; } }); // 獲取當(dāng)前時(shí)間 function getCurrentTime() { let now = new Date(); // 獲取當(dāng)前日期和時(shí)間 let hours = now.getHours(); // 獲取小時(shí) let minutes: string | number = now.getMinutes(); // 獲取分鐘 let seconds: string | number = now.getSeconds(); // 獲取秒 let period = hours >= 12 ? '下午' : '上午'; // 判斷是上午還是下午 // 將小時(shí)轉(zhuǎn)換為12小時(shí)制 hours = hours % 12 || 12; // 格式化分鐘和秒,確保它們是兩位數(shù) minutes = minutes < 10 ? '0' + minutes : minutes; seconds = seconds < 10 ? '0' + seconds : seconds; // 構(gòu)造最終的時(shí)間字符串 let currentTime = period + hours + ':' + minutes + ':' + seconds; return '【' + currentTime + '】 '; } //計(jì)算錯(cuò)誤信息所在行列 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 }; } //json格式美化 function prettyFormat(str) { try { // 設(shè)置縮進(jìn)為2個(gè)空格 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')) { editorHeight.value = JSON.parse(JSON.stringify(props.height)); editorWidth.value = JSON.parse(JSON.stringify(props.width)); center.value.className = center.value.className.replace(' fullScreen', ''); isFullScreen.value = false; } else { editorHeight.value = '100vh'; editorWidth.value = '100vw'; 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); } else if (event.key === 'Escape') { if (isFullScreen.value) { fullScreen(); } } } } // 符號(hào)自動(dòng)補(bǔ)全以及選中文本后輸入符號(hào)自動(dòng)包裹 function getMouseCheck(event) { setAutoKey(viewJsonStr, event); } onMounted(() => { window.addEventListener('keydown', handleKeyDown); document.addEventListener('keydown', getMouseCheck); calculateNum(props.value); }); onBeforeUnmount(() => { window.removeEventListener('keydown', handleKeyDown); document.removeEventListener('keydown', getMouseCheck); }); </script> <style scoped lang="less"> #editor_body { border: 1px solid #d9d9d9; border-radius: 4px; padding: 5px; box-sizing: border-box; } .tool_slider { padding-left: 5px; padding-right: 5px; display: flex; width: 100%; box-sizing: border-box; justify-content: space-between; align-items: center; height: 25px; border: 1px solid #d9d9d9; border-bottom: 0; } .icon_hover { height: 20px; width: 20px; cursor: pointer; &:hover { color: #5c82ff; } } #leftNum { overflow: hidden; padding: 3px 2px; height: 100%; 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; border-right: 1px solid; font-family: 微軟雅黑; } #rightNum { white-space: nowrap; height: 100%; padding: 3px; line-height: 22px; box-sizing: border-box; resize: none; border: 0; font-family: 微軟雅黑; &::-webkit-scrollbar { width: 5px; height: 5px; background-color: #efeae6; } &:focus-visible { outline: none; } &:hover { border: 0; } &:focus { border: 0; } } .leftBox { height: 100%; text-align: left; } .edit-container { height: calc(100% - 25px); width: 100%; box-sizing: border-box; border: 1px solid #d9d9d9; display: flex; } .fullScreen { position: fixed; z-index: 9999; left: 0; top: 0; right: 0; bottom: 0; background-color: #f5f7fa; } #console { padding: 12px; box-sizing: border-box; height: calc(20% - 5px); margin-top: 5px; width: 100%; background-color: white; border: 1px solid #d9d9d9; overflow: auto; font-family: 微軟雅黑; text-align: left; } </style>
配置文件
/*index.ts*/ import { nextTick } from 'vue'; // 獲取文本框的值 export const handleTextareaInput = (viewJsonStr, event) => { const textarea = event.target; const newValue = textarea.value; viewJsonStr.value = newValue; }; // 設(shè)置自動(dòng)補(bǔ)全 export const setAutoKey = (viewJsonStr, event) => { const textarea: any = document.getElementById('rightNum'); if (event.key === "'" || event.key === '"') { event.preventDefault(); // 阻止默認(rèn)行為 const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd); const newText = `${event.key}` + selectedText + `${event.key}`; const cursorPosition = textarea.selectionStart + 1; textarea.value = textarea.value.substring(0, textarea.selectionStart) + newText + textarea.value.substring(textarea.selectionEnd); textarea.setSelectionRange(cursorPosition, cursorPosition); } else if (event.key === '(') { event.preventDefault(); // 阻止默認(rèn)行為 const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd); const newText = '(' + selectedText + ')'; const cursorPosition = textarea.selectionStart + 1; textarea.value = textarea.value.substring(0, textarea.selectionStart) + newText + textarea.value.substring(textarea.selectionEnd); textarea.setSelectionRange(cursorPosition, cursorPosition); } else if (event.key === '[') { event.preventDefault(); // 阻止默認(rèn)行為 const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd); const newText = '[' + selectedText + ']'; const cursorPosition = textarea.selectionStart + 1; textarea.value = textarea.value.substring(0, textarea.selectionStart) + newText + textarea.value.substring(textarea.selectionEnd); textarea.setSelectionRange(cursorPosition, cursorPosition); } else if (event.key === '{') { event.preventDefault(); // 阻止默認(rèn)行為 const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd); const newText = '{' + selectedText + '}'; const cursorPosition = textarea.selectionStart + 1; textarea.value = textarea.value.substring(0, textarea.selectionStart) + newText + textarea.value.substring(textarea.selectionEnd); textarea.setSelectionRange(cursorPosition, cursorPosition); } viewJsonStr.value = textarea.value; }; /*------------------------------------------------括號(hào)高亮------------------------------------------------------------*/ 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); }; // 點(diǎn)擊括號(hào)尋找對(duì)應(yīng)另一半 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; // 獲取光標(biāo)位置 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; // 將光標(biāo)移動(dòng)到插入的空格后面 setTimeout(() => { handleTabKey(syntheticEvent); }, 30); } } } // 新建tab按鍵對(duì)象 const syntheticEvent = new KeyboardEvent('keydown', { key: 'Tab', }); // 按下tab鍵時(shí)的操作 export const handleTabKey = (event) => { const textarea: any = document.getElementById('rightNum'); const { selectionStart, selectionEnd } = textarea; const tabSpaces = ' '; // 4 spaces event.preventDefault(); // 在當(dāng)前光標(biāo)位置插入4個(gè)空格 textarea.value = textarea.value.substring(0, selectionStart) + tabSpaces + textarea.value.substring(selectionEnd); // 將光標(biāo)向右移動(dòng)4個(gè)空格 textarea.selectionStart = selectionStart + tabSpaces.length; textarea.selectionEnd = selectionStart + tabSpaces.length; }; // 按下Backspace按鍵時(shí) 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(); // 阻止默認(rèn)的刪除行為 viewJsonStr.value = textBeforeCursor.slice(0, -1) + textAfterCursor.slice(1); nextTick(() => { textarea.selectionStart = cursorPosition - 1; textarea.selectionEnd = cursorPosition - 1; }).then((r) => {}); } }
調(diào)用方式
<JsonEditor v-model:value="testStr" /> const testStr = ref('123');
總結(jié)
這個(gè)JSON編輯器不僅能夠讓你方便地格式化JSON字符串,還能幫你去掉不必要的空格。而且,它的全屏功能讓編輯更加順暢。最酷的是,它還能實(shí)時(shí)告訴你格式化的進(jìn)度,如果遇到問題了,控制臺(tái)會(huì)詳細(xì)告訴你哪里出錯(cuò)了,這樣你就能快速找到問題并解決它。編輯器還能精確地計(jì)算行號(hào),這對(duì)于查找問題也是很有幫助的。而且,它還有自動(dòng)補(bǔ)全、自動(dòng)刪除和括號(hào)匹配這些貼心的功能,讓你的編輯工作變得更加輕松。如果你不小心做錯(cuò)了,也不用擔(dān)心,因?yàn)樗С殖蜂N和重做。希望它能幫助到大家,讓我們的工作更加愉快!
到此這篇關(guān)于Vue實(shí)現(xiàn)JSON字符串格式化編輯器組件的文章就介紹到這了,更多相關(guān)Vue JSON字符串格式化編輯器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解vue2.0 transition 多個(gè)元素嵌套使用過渡
這篇文章主要介紹了詳解vue2.0 transition 多個(gè)元素嵌套使用過渡,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06在?Vue?中使用?dhtmlxGantt?組件時(shí)遇到的問題匯總(推薦)
dhtmlxGantt一個(gè)功能豐富的甘特圖插件,支持任務(wù)編輯,資源分配和多種視圖模式,這篇文章主要介紹了在?Vue?中使用?dhtmlxGantt?組件時(shí)遇到的問題匯總,需要的朋友可以參考下2023-03-03vue pages 多入口項(xiàng)目 + chainWebpack 全局引用縮寫說明
這篇文章主要介紹了vue pages 多入口項(xiàng)目 + chainWebpack 全局引用縮寫說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09Vue中this.$router.push參數(shù)獲取方法
下面小編就為大家分享一篇Vue中this.$router.push參數(shù)獲取方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-02-02vue移動(dòng)端下拉刷新和上拉加載的實(shí)現(xiàn)代碼
這篇文章主要介紹了vue移動(dòng)端下拉刷新和上拉加載的實(shí)現(xiàn)代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-09-09