终于终于忙完啦!天天加班,天天加班遭不住了啊!终于呦呦有空写点东西了。
上一阵子接到一个需求,让我搞一个可以格式美化json和yaml数据格式的编辑器,当时一听就头大,什么编辑器?什么是yaml?什么格式化?json倒是还不陌生。
这次主要都是干货,直接看代码,话不多说,开搞!
一、先看效果
1.校验成功
2.校验失败,失败时候会有报错信息,打印报错行
3.组件中可以传入很多控制参数
例如:
lang :语言配置 json/yaml
disabled:是否禁用
showConsole: 是否展示控制台
menuList:string[] 菜单项 "clearSpaces", "example", "preview", "fullScreen", "copy"
exampleValue: 示例代码 menuList包含example时有效
......等等
我就不一一列举了,感兴趣的小伙伴可以按照我上面的结构图copy下来使用。
4.扩展
其实还有很多功能可以进行扩展,我这里只是满足了当前的项目需求,感兴趣的同学可以把下面的代码COPY下来自己延伸一下。
相信有很多大佬可以扩展将其扩展的琳琅满目。
二、代码结构
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>