Vue实现JSON编辑器

效果图

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'; //判断浏览器是否是IE
        } 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 };
  }

  //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')) {
        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);
    }
  }
}
// 新建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) => {});
  }
}

调用方法
<json-editor v-model:json-str="formData.sessionToolConfig" :rows="8" />
传参

v-model:json-str:双向绑定编辑器的值和使用的变量

rows:编辑器的行数

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值