实现一个json和yaml编辑器组件

559 阅读5分钟

终于终于忙完啦!天天加班,天天加班遭不住了啊!终于呦呦有空写点东西了。

02339855.jpg

上一阵子接到一个需求,让我搞一个可以格式美化json和yaml数据格式的编辑器,当时一听就头大,什么编辑器?什么是yaml?什么格式化?json倒是还不陌生。

这次主要都是干货,直接看代码,话不多说,开搞!

一、先看效果

1.校验成功

image.png

2.校验失败,失败时候会有报错信息,打印报错行

image.png

3.组件中可以传入很多控制参数

例如:

lang :语言配置 json/yaml

disabled:是否禁用

showConsole: 是否展示控制台

menuList:string[] 菜单项 "clearSpaces", "example", "preview", "fullScreen", "copy"

exampleValue: 示例代码 menuList包含example时有效

......等等

我就不一一列举了,感兴趣的小伙伴可以按照我上面的结构图copy下来使用。

4.扩展

其实还有很多功能可以进行扩展,我这里只是满足了当前的项目需求,感兴趣的同学可以把下面的代码COPY下来自己延伸一下。

相信有很多大佬可以扩展将其扩展的琳琅满目。

016271AB.jpg

二、代码结构

image.png

1.前期准备安装

npm i vue-codemirror

npm i @codemirror/lang-json //json语言支持

npm i @codemirror/lang-yaml //yaml语言支持

npm i @codemirror/lang-javascript // 可以不安装,需要的时候再安装

npm i --save ant-design-vue //组件库

2.上代码

langEditor.vue组件

<template>
  <div :class="['editor', isFullScreen ? 'editor-fullScreen' : '', props.disabled ? 'editor-disabled' : '']" :style="{ height: height + 'px' }">
    <div class="top_menu">
      <a-tooltip title="格式美化" :placement="isFullScreen ? 'bottom' : 'top'" :getPopupContainer="getPopupContainer">
        <FontSizeOutlined class="icon_hover" @click="formatFn" />
      </a-tooltip>
      <template v-if="props.menuList?.includes('clearSpaces') && props.lang === 'json'">
        <a-divider type="vertical" />
        <a-tooltip title="清除空格" :placement="isFullScreen ? 'bottom' : 'top'" :getPopupContainer="getPopupContainer">
          <MergeCellsOutlined class="icon_hover" @click="handleClearSpaces" />
        </a-tooltip>
      </template>
      <template v-if="props.menuList?.includes('example')">
        <a-divider type="vertical" />
        <a-tooltip title="示例参数" :placement="isFullScreen ? 'bottom' : 'top'" :getPopupContainer="getPopupContainer">
          <FileDoneOutlined class="icon_hover" @click="handleExample" />
        </a-tooltip>
      </template>
      <template v-if="props.menuList?.includes('preview') && !isFullScreen">
        <a-divider type="vertical" />
        <a-tooltip title="预览" :placement="isFullScreen ? 'bottom' : 'top'" :getPopupContainer="getPopupContainer">
          <EyeOutlined class="icon_hover" @click="handlePreview" />
        </a-tooltip>
      </template>
      <a-divider type="vertical" />
      <a-tooltip title="清除内容" :placement="isFullScreen ? 'bottom' : 'top'" :getPopupContainer="getPopupContainer">
        <ClearOutlined class="icon_hover" @click="handleClear" />
      </a-tooltip>
      <template v-if="props.menuList?.includes('fullScreen')">
        <a-divider type="vertical" />
        <a-tooltip v-if="!isFullScreen" title="全屏" :placement="isFullScreen ? 'bottom' : 'top'" :getPopupContainer="getPopupContainer">
          <FullscreenOutlined class="icon_hover full_icon_hover" @click="fullScreenFn" />
        </a-tooltip>
        <a-tooltip v-else title="退出全屏" :placement="isFullScreen ? 'bottom' : 'top'" :getPopupContainer="getPopupContainer">
          <FullscreenExitOutlined class="icon_hover full_icon_hover" @click="fullScreenFn" />
        </a-tooltip>
      </template>
      <template v-if="props.menuList?.includes('copy')">
        <a-divider type="vertical" />
        <a-tooltip title="复制" :placement="isFullScreen ? 'bottom' : 'top'" :getPopupContainer="getPopupContainer">
          <CopyOutlined class="icon_hover copy_icon_hover" @click="() => textCopy(code)" />
        </a-tooltip>
      </template>
      <div v-if="code" class="status-icon">
        <CheckCircleOutlined v-if="isCheck" style="color: green" />
        <CloseCircleOutlined v-else style="color: red" />
      </div>
    </div>
    <codemirror
      ref="mycodemirror"
      v-model="code"
      :disabled="props.disabled"
      :extensions="extensions"
      :indentWithTab="true"
      :placeholder="placeholder"
      :tabSize="2"
      :style="{ height: props.showConsole ? 'calc(80% - 45px)' : 'calc(100% - 45px)', border: '1px solid #d9d9d9', outline: 'none !important' }"
      @blur="() => emits('handleValidate')"
    />
    <div v-if="props.showConsole" :class="['bottom_console', isCheck ? 'success-msg' : 'fail-msg']">
      <p v-if="code">{{ resultMsg }}</p>
    </div>
  </div>
</template>
<script lang="ts" setup>
import { Codemirror } from 'vue-codemirror'
import { EditorView } from '@codemirror/view'
import { autocompletion } from '@codemirror/autocomplete'
import { yaml } from '@codemirror/lang-yaml'
import { json } from '@codemirror/lang-json'
import { javascript } from '@codemirror/lang-javascript'
import { ref, nextTick, defineExpose, computed, watch } from 'vue'
import {
  FontSizeOutlined,
  FullscreenExitOutlined,
  FullscreenOutlined,
  CheckCircleOutlined,
  CloseCircleOutlined,
  ClearOutlined,
  FileDoneOutlined,
  EyeOutlined,
  MergeCellsOutlined,
  CopyOutlined,
} from '@ant-design/icons-vue'
import { beautifyText, getCurrentTime, textCopy } from './utils'

interface IProps {
  value: string
  placeholder: string
  height: number
  lang: 'json' | 'yaml'
  menuList: string[]
  exampleValue: string
  disabled: boolean
  showConsole: boolean
}

const props: IProps = defineProps({
  value: {
    type: String,
    default: '',
  },
  placeholder: {
    type: String,
    default: '请输入json格式的数据',
  },
  height: {
    type: Number,
    default: 500,
  },
  lang: {
    validator(value) {
      return ['json', 'yaml'].includes(value)
    },
    default: 'json',
  },
  menuList: {
    // 附加功能 "clearSpaces", "example", "preview", "fullScreen", "copy"
    type: Array,
    default: () => [],
  },
  exampleValue: {
    // 示例代码
    type: String,
    default: '',
  },
  disabled: {
    // 是否禁用
    type: Boolean,
    default: false,
  },
  showConsole: {
    // 是否显示报错信息
    type: Boolean,
    default: true,
  },
})

const emits = defineEmits(['update:value', 'handlePreview', 'handleClear', 'handleValidate'])
const extensions = [EditorView.lineWrapping, autocompletion()]
if (props.lang === 'yaml') {
  extensions.push(yaml())
} else if (props.lang === 'javascript') {
  extensions.push(javascript())
} else {
  extensions.push(json())
}
const resultMsg = ref('')
const isCheck = ref(true)
const code = computed({
  get: () => props.value,
  set: (val) => {
    emits('update:value', val)
  },
})
const isFullScreen = ref(false)

/**
 * 格式化文本
 * @param assignment 是否赋值
 */
const formatFn = (assignment = true) => {
  if (props.disabled) return
  beautifyText(
    props.lang || 'json',
    code.value,
    (text: string) => {
      if (assignment) code.value = text
      isCheck.value = true
      resultMsg.value = getCurrentTime() + '校验通过'
    },
    (error: any) => {
      isCheck.value = false
      resultMsg.value = getCurrentTime() + error.message
    }
  )
}

// 清除空格
const handleClearSpaces = () => {
  if (props.disabled) return
  code.value = code.value.replace(/\s+/g, '')
}

// 清空内容
const handleClear = () => {
  if (props.disabled) return
  code.value = ''
  emits('handleClear')
}

// 示例参数
const handleExample = () => {
  if (props.disabled) return
  code.value = props.exampleValue
  nextTick(() => {
    formatFn()
  })
}

// 预览
const handlePreview = () => {
  if (props.disabled) return
  emits('handlePreview')
}

// 全屏
const fullScreenFn = () => {
  // if(props.disabled) return;
  isFullScreen.value = !isFullScreen.value
}

const getPopupContainer = (trigger: HTMLElement) => {
  return trigger.parentElement
}

watch(
  () => code.value,
  () => {
    formatFn(false)
  },
  {
    immediate: true,
  }
)

watch(
  () => code.value,
  () => {
    formatFn(false)
    emits('handleValidate')
  }
)

defineExpose({
  beautifyText,
})
</script>

<style>
.ant-tooltip {
  z-index: 99999 !important;
}
</style>

<style scoped lang="scss">
@use './index.scss';
</style>

utils.ts核心文件

import * as jsyaml from 'js-yaml';
import { message } from "ant-design-vue";

// 获取当前时间
export const 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 + '】 ';
}

/**
 * 格式化文本
 * @param lang 格式化的类型
 * @param text 格式化的文本
 * @param okFunction 成功的回调
 * @param failFunction 失败的回调
 * @returns  状态
 */
export const beautifyText = (lang: string, text: string, okFunction?: Function, failFunction?: Function) => {
  try {
    if (lang === 'yaml') {
      // 将YAML字符串解析为JavaScript对象  
      const data = jsyaml.load(text);
      // 将JavaScript对象转换回YAML字符串,并美化输出  
      const beautifiedYaml = jsyaml.dump(data, { indent: 2 }); // indent参数用于控制缩进  
      // 你可以在这里使用beautifiedYaml,比如显示在组件的模板中  
      okFunction && okFunction(beautifiedYaml)
    } else if (lang === 'json') {
      // 设置缩进为2个空格
      let data = JSON.stringify(JSON.parse(text), null, 4);
      data = data.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
      const beautifiedJson = data.replace(
        /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
        function (match) {
          return match;
        },
      );
      okFunction && okFunction(beautifiedJson)
    }
    return true
  } catch (error) {
    failFunction && failFunction(error)
    return false
  }
}

/**
 * 文本复制
 * @param t
 */
export function textCopy(t) {
  // 如果当前浏览器版本不兼容navigator.clipboard
  if (!navigator.clipboard) {
    const ele = document.createElement("input");
    ele.value = t;
    document.body.appendChild(ele);
    ele.select();
    document.execCommand("copy");
    document.body.removeChild(ele);
    if (document.execCommand("copy")) {
      message.success("复制成功!");
    } else {
      message.error("复制失败!");
    }
  } else {
    navigator.clipboard
      .writeText(t)
      .then(function () {
        message.success("复制成功!");
      })
      .catch(function () {
        message.error("复制失败!");
      });
  }
}

langEditor.scss

.editor {
    border: 1px solid #d9d9d9;
    padding: 10px;

    .top_menu {
        padding: 10px;
        border: 1px solid #d9d9d9;
        border-bottom: none;
        position: relative;

        .icon_hover {
            cursor: pointer;

            &:hover {
                color: #5c82ff;
            }
        }

        .status-icon {
            position: absolute;
            top: 10px;
            right: 10px;
        }
    }

    .bottom_console {
        margin-top: 5px;
        padding: 10px;
        border: 1px solid #d9d9d9;
        height: 20%;
        overflow-y: auto;
    }

    .success-msg {
        color: green;
    }

    .fail-msg {
        color: red;
    }
}

.editor-fullScreen {
    position: fixed;
    height: 100vh !important;
    z-index: 9999;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    background-color: #f5f7fa;
}

.editor-disabled {
    .top_menu {
        .icon_hover {
            cursor: no-drop;
            &:hover {
                color: rgba(0, 0, 0, 0.88);
            }
        }
        .copy_icon_hover, .full_icon_hover{
            cursor: pointer;
            &:hover{
                color: #5c82ff;
            }
        }
    }
}

使用

<template>
  <div class="json-box">
    <langEditor
      v-model:value="EditValue"
      placeholder="请输入请求参数"
      lang="json"
      :menuList="['clearSpaces', 'fullScreen', 'copy']"
      :height="300"
      :showConsole="true"
    />
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import langEditor from '@/components/langEditor/index.vue'

const EditValue = ref('')
</script>
<style scoped lang="scss">
.json-box {
  width: 500px;
  height: 500px;
}
</style>