Vue實現(xiàn)JSON字符串格式化編輯器組件功能
相信很多同學(xué)都用過網(wǎng)上的在線JSON格式化工具來將雜亂的JSON數(shù)據(jù)轉(zhuǎn)換成易于我們閱讀和編輯的格式。那么,你有沒有想過自己動手實現(xiàn)一個這樣的工具呢?今天,我將介紹如何使用Vue.js來構(gòu)建一個簡單的JSON格式化工具。
功能簡述
- 支持格式化JSON字符串
- 支持去除字符串中的空格
- 支持全屏操作
- 實時展示格式化狀態(tài)
- 控制臺展示成功和失敗的詳情,支持錯誤定位
- 編輯器精準(zhǔn)計算字符串的行號
效果圖展示
默認(rèn)


全屏

功能介紹
按鈕

其他
1、自動補(bǔ)全
輸入”(“、”{“、”[“將會自動補(bǔ)全另一半
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)));
// 自動補(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ù)覽對象
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'));
// 如果當(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)前時間
function getCurrentTime() {
let now = new Date(); // 獲取當(dāng)前日期和時間
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è)置縮進(jìn)為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();
}
}
}
}
// 符號自動補(bǔ)全以及選中文本后輸入符號自動包裹
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è)置自動補(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;
};
/*------------------------------------------------括號高亮------------------------------------------------------------*/
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; // 獲取光標(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)移動到插入的空格后面
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();
// 在當(dāng)前光標(biāo)位置插入4個空格
textarea.value =
textarea.value.substring(0, selectionStart) +
tabSpaces +
textarea.value.substring(selectionEnd);
// 將光標(biāo)向右移動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(); // 阻止默認(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é)
這個JSON編輯器不僅能夠讓你方便地格式化JSON字符串,還能幫你去掉不必要的空格。而且,它的全屏功能讓編輯更加順暢。最酷的是,它還能實時告訴你格式化的進(jìn)度,如果遇到問題了,控制臺會詳細(xì)告訴你哪里出錯了,這樣你就能快速找到問題并解決它。編輯器還能精確地計算行號,這對于查找問題也是很有幫助的。而且,它還有自動補(bǔ)全、自動刪除和括號匹配這些貼心的功能,讓你的編輯工作變得更加輕松。如果你不小心做錯了,也不用擔(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-03
vue pages 多入口項目 + chainWebpack 全局引用縮寫說明
這篇文章主要介紹了vue pages 多入口項目 + chainWebpack 全局引用縮寫說明,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09
Vue中this.$router.push參數(shù)獲取方法
下面小編就為大家分享一篇Vue中this.$router.push參數(shù)獲取方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-02-02

