Vue實現(xiàn)JSON字符串格式化編輯器組件功能
相信很多同學(xué)都用過網(wǎng)上的在線JSON格式化工具來將雜亂的JSON數(shù)據(jù)轉(zhuǎn)換成易于我們閱讀和編輯的格式。那么,你有沒有想過自己動手實現(xiàn)一個這樣的工具呢?今天,我將介紹如何使用Vue.js來構(gòu)建一個簡單的JSON格式化工具。
功能簡述
- 支持格式化JSON字符串
- 支持去除字符串中的空格
- 支持全屏操作
- 實時展示格式化狀態(tài)
- 控制臺展示成功和失敗的詳情,支持錯誤定位
- 編輯器精準計算字符串的行號
效果圖展示
默認
全屏
功能介紹
按鈕
其他
1、自動補全
輸入”(“、”{“、”[“將會自動補全另一半
2、自動刪除
刪除括號時也會自動刪除另一半
3、括號匹配
點擊括號會高亮另一半括號,方便定位
4、支持ctrl+z撤銷和ctrl+y重做功能
5、編輯器根據(jù)字符串的換行計算行號并展示
代碼
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="格式錯誤" 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="請輸入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))); // 自動補全 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ù)覽對象 const jsonObj = computed(() => { const str = cloneDeep(viewJsonStr.value); // 如果輸入的全是數(shù)字,JSON.parse(str)不會報錯,需要手動處理一下 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()}校驗通過`; } } 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')); // 如果當前行或者前一行有'[' 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; } }); // 獲取當前時間 function getCurrentTime() { let now = new Date(); // 獲取當前日期和時間 let hours = now.getHours(); // 獲取小時 let minutes: string | number = now.getMinutes(); // 獲取分鐘 let seconds: string | number = now.getSeconds(); // 獲取秒 let period = hours >= 12 ? '下午' : '上午'; // 判斷是上午還是下午 // 將小時轉(zhuǎn)換為12小時制 hours = hours % 12 || 12; // 格式化分鐘和秒,確保它們是兩位數(shù) minutes = minutes < 10 ? '0' + minutes : minutes; seconds = seconds < 10 ? '0' + seconds : seconds; // 構(gòu)造最終的時間字符串 let currentTime = period + hours + ':' + minutes + ':' + seconds; return '【' + currentTime + '】 '; } //計算錯誤信息所在行列 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è)置縮進為2個空格 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(); } } } } // 符號自動補全以及選中文本后輸入符號自動包裹 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è)置自動補全 export const setAutoKey = (viewJsonStr, event) => { const textarea: any = document.getElementById('rightNum'); if (event.key === "'" || event.key === '"') { event.preventDefault(); // 阻止默認行為 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(); // 阻止默認行為 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(); // 阻止默認行為 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(); // 阻止默認行為 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; }; /*------------------------------------------------括號高亮------------------------------------------------------------*/ 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); }; // 點擊括號尋找對應(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; // 獲取光標位置 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); } } } // 新建tab按鍵對象 const syntheticEvent = new KeyboardEvent('keydown', { key: 'Tab', }); // 按下tab鍵時的操作 export const handleTabKey = (event) => { const textarea: any = document.getElementById('rightNum'); const { selectionStart, selectionEnd } = textarea; const tabSpaces = ' '; // 4 spaces event.preventDefault(); // 在當前光標位置插入4個空格 textarea.value = textarea.value.substring(0, selectionStart) + tabSpaces + textarea.value.substring(selectionEnd); // 將光標向右移動4個空格 textarea.selectionStart = selectionStart + tabSpaces.length; textarea.selectionEnd = selectionStart + tabSpaces.length; }; // 按下Backspace按鍵時 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) => {}); } }
調(diào)用方式
<JsonEditor v-model:value="testStr" /> const testStr = ref('123');
總結(jié)
這個JSON編輯器不僅能夠讓你方便地格式化JSON字符串,還能幫你去掉不必要的空格。而且,它的全屏功能讓編輯更加順暢。最酷的是,它還能實時告訴你格式化的進度,如果遇到問題了,控制臺會詳細告訴你哪里出錯了,這樣你就能快速找到問題并解決它。編輯器還能精確地計算行號,這對于查找問題也是很有幫助的。而且,它還有自動補全、自動刪除和括號匹配這些貼心的功能,讓你的編輯工作變得更加輕松。如果你不小心做錯了,也不用擔(dān)心,因為它支持撤銷和重做。希望它能幫助到大家,讓我們的工作更加愉快!
到此這篇關(guān)于Vue實現(xiàn)JSON字符串格式化編輯器組件的文章就介紹到這了,更多相關(guān)Vue JSON字符串格式化編輯器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解vue2.0 transition 多個元素嵌套使用過渡
這篇文章主要介紹了詳解vue2.0 transition 多個元素嵌套使用過渡,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06在?Vue?中使用?dhtmlxGantt?組件時遇到的問題匯總(推薦)
dhtmlxGantt一個功能豐富的甘特圖插件,支持任務(wù)編輯,資源分配和多種視圖模式,這篇文章主要介紹了在?Vue?中使用?dhtmlxGantt?組件時遇到的問題匯總,需要的朋友可以參考下2023-03-03vue pages 多入口項目 + chainWebpack 全局引用縮寫說明
這篇文章主要介紹了vue pages 多入口項目 + chainWebpack 全局引用縮寫說明,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09Vue中this.$router.push參數(shù)獲取方法
下面小編就為大家分享一篇Vue中this.$router.push參數(shù)獲取方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-02-02