赞
踩
相信很多同学都用过网上的在线JSON格式化工具来将杂乱的JSON数据转换成易于我们阅读和编辑的格式。那么,你有没有想过自己动手实现一个这样的工具呢?今天,我将介绍如何使用Vue.js来构建一个简单的JSON格式化工具。
1、自动补全
输入”(“、”{“、”[“将会自动补全另一半
2、自动删除
删除括号时也会自动删除另一半
3、括号匹配
点击括号会高亮另一半括号,方便定位
4、支持ctrl+z撤销和ctrl+y重做功能
5、编辑器根据字符串的换行计算行号并展示
- <!--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;
- }
-
- // 预览对象
- const jsonObj = computed(() => {
- const str = cloneDeep(viewJsonStr.value);
- // 如果输入的全是数字,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 ? '下午' : '上午'; // 判断是上午还是下午
-
- // 将小时转换为12小时制
- hours = hours % 12 || 12;
-
- // 格式化分钟和秒,确保它们是两位数
- minutes = minutes < 10 ? '0' + minutes : minutes;
- seconds = seconds < 10 ? '0' + seconds : seconds;
-
- // 构造最终的时间字符串
- 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 {
- // 设置缩进为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;
- };
-
- // 设置自动补全
- 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);
- };
- // 点击括号寻找对应另一半
- 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) => {});
- }
- }
- <JsonEditor v-model:value="testStr" />
-
- const testStr = ref('123');
这个JSON编辑器不仅能够让你方便地格式化JSON字符串,还能帮你去掉不必要的空格。而且,它的全屏功能让编辑更加顺畅。最酷的是,它还能实时告诉你格式化的进度,如果遇到问题了,控制台会详细告诉你哪里出错了,这样你就能快速找到问题并解决它。编辑器还能精确地计算行号,这对于查找问题也是很有帮助的。而且,它还有自动补全、自动删除和括号匹配这些贴心的功能,让你的编辑工作变得更加轻松。如果你不小心做错了,也不用担心,因为它支持撤销和重做。希望它能帮助到大家,让我们的工作更加愉快!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。