前言
使用monaco-editor
作为代码编辑器,echarts
预览采用iframe
引入一个页面形式/独立的一个容器内不影响外部的操作
演示视频
[video(video-3pHTn7yR-1727592280528)(type-csdn)(url-live.csdn.net/v/embed/427…)]
一、布局和大小设置
1.布局
布局分左右两侧,左侧编辑代码和运行代码;
右侧对代码的预览,和echarts
版本的切换;
<div class="container-main" ref="MainRef" :class="{ 'isDrag': isDrag }">
<div class="container-main-code" ref="rCode">
<div class="container-main-codeView-header">
<div class="RunCode" @click="handleClickRun">运行</div>
</div>
<div class="container-main-codeView-content">
<codeEditor @init="handleInitCode" ref="iediter"></codeEditor>
</div>
</div>
<div class="container-main-move" @mousedown="mouseDown" ref="MoveRef"></div>
<div class="container-main-view" ref="rView">
<div class="container-main-codeView-header">
<select class="container-main-view-select" v-model="version" style="width: 120px" @change="versionChange">
<option :value="item" v-for="item in versionOption">{{ item }}</option>
</select>
</div>
<div class="container-main-codeView-content" ref="frameContent">
<iframe :src="iframeSrc" id="Frame" class="container-main-view-frame"></iframe>
</div>
</div>
</div>
<style lang="less" scoped>
.container-main {
width: 100%;
height: 100%;
display: flex;
position: relative;
.container-main-codeView-content {
flex: 1;
height: 0px;
overflow: hidden;
}
.container-main-codeView-header {
width: 100%;
display: flex;
box-sizing: border-box;
padding: 6px 12px;
height: calc(12px + 32px);
min-height: calc(12px + 32px);
max-height: calc(12px + 32px);
background-color: #fff;
box-shadow: 0px 1px 4px #1677ff8a;
z-index: 9999;
.RunCode {
height: 32px;
width: 80px;
background-color: #1677ff;
color: #fff;
border-radius: 6px;
align-items: center;
justify-content: center;
display: flex;
cursor: pointer;
font-size: 14px;
}
}
.container-main-code {
width: 45%;
height: 100%;
z-index: 60;
display: flex;
flex-direction: column;
}
.container-main-move {
width: 12px;
left: 45%;
height: 100%;
position: absolute;
cursor: col-resize;
z-index: 999;
background-color: #f1f1f1;
}
.container-main-view {
width: 55%;
height: 100%;
padding-left: 12px;
box-sizing: border-box;
z-index: 60;
display: flex;
flex-direction: column;
.container-main-view-select{
border-color: #1677ff;
border-radius: 6px;
height: 32px;
outline: none;
font-weight: 600;
padding: 0px .5em;
font-size: 14px;
}
:deep(.container-main-view-frame) {
width: 100%;
height: 100%;
display: block;
border: none;
outline: none;
}
}
}
.isDrag::after {
position: absolute;
width: 100%;
height: 100%;
content: '';
left: 0;
top: 0;
z-index: 9999;
cursor: col-resize;
}
</style>
2.大小调整
可以看到<div class="container-main-move" @mousedown="mouseDown" ref="MoveRef"></div>
该元素是用来,通过鼠标按下滑动设置,代码编辑和预览视图的大小;
let MoveRef = ref();
let MainRef = ref();
let startDifMove = ref(0);
let isDrag = ref(false);
let rCode = ref();
let rView = ref();
let iediter = ref();
const mouseMove = (event: MouseEvent) => {
let movex = event.clientX - startDifMove.value;
let codePie: number;
let viewPie: number;
let maxWidth = MainRef.value.clientWidth;
codePie = ((movex / maxWidth) * 100)
viewPie = (100 - codePie)
if (codePie < 10) {
codePie = 10;
viewPie = 90;
}
if (viewPie < 10) {
codePie = 90
viewPie = 10;
}
MoveRef.value.style.left = codePie + '%';
rCode.value.style.width = codePie + '%';
rView.value.style.width = viewPie + '%';
iediter.value.resize();
}
const mouseUp = () => {
window.removeEventListener('mousemove', mouseMove);
window.removeEventListener('mouseup', mouseUp);
isDrag.value = false;
}
const mouseDown = (event: MouseEvent) => {
startDifMove.value = event.clientX - MoveRef.value.offsetLeft;
isDrag.value = true;
window.addEventListener('mousemove', mouseMove);
window.addEventListener('mouseup', mouseUp);
}
onUnmounted(() => {
mouseUp();
document.removeEventListener('visibilitychange', visibilitychangeFun)
})
const visibilitychangeFun = () => {
let visibilityState = document.visibilityState
if (visibilityState == 'visible') {
mouseUp();
}
}
document.addEventListener('visibilitychange', visibilitychangeFun)
二、代码编辑组件
左侧用来编辑代码的组件;暴露出,getValue resize方法,用来获取输入内容和调整大小
1.组件代码
<template>
<div class="monaco-editor" ref="editor"></div>
</template>
<script setup lang="ts">
import {code} from './defaultCode';
import * as monaco from 'monaco-editor';
import { ref, onMounted,toRaw,defineExpose,defineEmits } from 'vue';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
let editor = ref();
let instance = ref();
const emits = defineEmits(['init']);
let defaultCode = code;
const initEditor = () => {
instance.value = monaco.editor.create(editor.value, {
value: defaultCode,
language: 'javascript',
});
emits('init',getValue())
}
onMounted(()=>{
initEditor();
})
const getValue = () =>{
return toRaw(instance.value).getValue();
}
const resize = () =>{
toRaw(instance.value).layout();
}
// 解决 codeEditor 输入报错的
// https://github.com/microsoft/monaco-editor/issues/2122
window.MonacoEnvironment = {
getWorker (_: string, label: string) {
if (label === 'typescript' || label === 'javascript') return new TsWorker()
if (label === 'json') return new JsonWorker()
if (label === 'css') return new CssWorker()
if (label === 'html') return new HtmlWorker()
return new EditorWorker()
}
}
defineExpose({
getValue,
resize
})
window.onresize = () =>{
requestAnimationFrame(()=>{
resize();
})
}
</script>
<style lang="less" scoped>
.monaco-editor{
width: 100%;
height: 100%;
}
</style>
2.默认代码和base64
let _str = `bGV0IGRhdGEgPSBbCiAgIHsKICAgICAgbmFtZTogJ+WSqOivouexu+aVsOmHjycsCiAgICAgIHZhbHVlOiA1MDAwLAogICAgICBwcm86IDUwCiAgIH0sCiAgIHsKICAgICAgbmFtZTogJ+aKleivieexu+aVsOmHjycsCiAgICAgIHZhbHVlOiAzMDAwLAogICAgICBwcm86IDMwCiAgIH0sCiAgIHsKICAgICAgbmFtZTogJ+ivhOS7t+exu+aVsOmHjycsCiAgICAgIHZhbHVlOiAyMDAwLAogICAgICBwcm86IDIwCiAgIH0KXQpjb25zdCBwdXNoWmVybyA9IChuKSA9PiB7CiAgIGlmIChuICUgMTAgPT0gMCkgewogICAgICByZXR1cm4gbiArICcuMDAnCiAgIH0gZWxzZSB7CiAgICAgIHJldHVybiBuCiAgIH0KfQpsZXQgY29sb3JzID0gWycjM0I4Q0U5JywgJyM1QkMzNzQnLCAnI0VBNjY3MiddOwpsZXQgZGF0YU5hbWVNYXAgPSB7CiAgICflkqjor6LnsbvmlbDph48nOiAnenhsJywKICAgJ+aKleivieexu+aVsOmHjyc6ICd0c2wnLAogICAn6K+E5Lu357G75pWw6YePJzogJ3BqbCcKfQpsZXQgbGVnZW5kUmljaHMgPSB7CiAgIHRpdGxlMTogewogICAgICBjb2xvcjogJyM5OTk5OTknLAogICAgICB3aWR0aDogMTIwLAogICAgICBhbGlnbjogJ2xlZnQnLAogICAgICBoZWlnaHQ6IDE0LAogICAgICBmb250U2l6ZTogMTQsCiAgICAgIHBhZGRpbmc6IFsyLCAwLCAwLCA0XQogICB9LAogICB0aXRsZTI6IHsKICAgICAgaGVpZ2h0OiAxNCwKICAgICAgY29sb3I6ICcjOTk5OTk5JywKICAgICAgZm9udFNpemU6IDE0LAogICAgICBwYWRkaW5nOiBbMiwgMCwgMCwgMF0KICAgfSwKICAgemhhbjogewogICAgICBoZWlnaHQ6IDE0CiAgIH0sCiAgIG5zZWw6IHsKICAgICAgd2lkdGg6IDE0LAogICAgICBoZWlnaHQ6IDE0LAogICAgICBiYWNrZ3JvdW5kQ29sb3I6ICcjY2NjY2NjOGEnLAogICAgICBib3JkZXJSYWRpdXM6IDE0LAogICB9LAogICBudmFsdWUxOiB7CiAgICAgIHdpZHRoOiAxMjAsCiAgICAgIGZvbnRTaXplOiAxOCwKICAgICAgY29sb3I6ICcjOTk5OTk5JywKICAgICAgcGFkZGluZzogWzAsIDAsIDAsIDE0ICsgNF0KICAgfSwKICAgbnZhbHVlMjogewogICAgICBmb250U2l6ZTogMTgsCiAgICAgIGNvbG9yOiAnIzk5OTk5OScsCiAgIH0KfTsKbGV0IHNlbGVjdHMgPSB7fTsKbGV0IGxlZ2VuZCA9IFtdOwpjb25zdCBjb21wdXRlZExlZ2VuZFJpY2hzID0gKERBVEEpID0+IHsKICAgREFUQS5tYXAoKGl0ZW0sIGluZGV4KSA9PiB7CiAgICAgIGxlZ2VuZC5wdXNoKGl0ZW0ubmFtZSkKICAgICAgbGV0IG1hcEsgPSBkYXRhTmFtZU1hcFtpdGVtLm5hbWVdOwogICAgICBzZWxlY3RzW2l0ZW0ubmFtZV0gPSB0cnVlOwogICAgICBsZXQgX2NvbG9yID0gY29sb3JzW2luZGV4XQoKICAgICAgbGV0IGljb24gPSBgaWNvbl9gICsgbWFwSwogICAgICBsZWdlbmRSaWNoc1tpY29uXSA9IHsKICAgICAgICAgd2lkdGg6IDE0LAogICAgICAgICBoZWlnaHQ6IDE0LAogICAgICAgICBiYWNrZ3JvdW5kQ29sb3I6IF9jb2xvciwKICAgICAgICAgYm9yZGVyUmFkaXVzOiAxNCwKICAgICAgfQoKICAgICAgbGV0IHZhbHVlMSA9ICd2YWx1ZTFfJyArIG1hcEs7CiAgICAgIGxldCB2YWx1ZTIgPSAndmFsdWUyXycgKyBtYXBLOwogICAgICBsZWdlbmRSaWNoc1t2YWx1ZTFdID0gewogICAgICAgICB3aWR0aDogMTIwLAogICAgICAgICBmb250U2l6ZTogMTgsCiAgICAgICAgIGNvbG9yOiBfY29sb3IsCiAgICAgICAgIHBhZGRpbmc6IFswLCAwLCAwLCAxNCArIDRdCiAgICAgIH0KICAgICAgbGVnZW5kUmljaHNbdmFsdWUyXSA9IHsKICAgICAgICAgZm9udFNpemU6IDE4LAogICAgICAgICBjb2xvcjogX2NvbG9yLAogICAgICB9CiAgIH0pCn0KCmNvbXB1dGVkTGVnZW5kUmljaHMoZGF0YSk7CgpvcHRpb24gPSB7CiAgIC8v5L2g55qE5Luj56CBCiAgIGNvbG9yOiBjb2xvcnMsCiAgIGJhY2tncm91bmRDb2xvcjogIiNmZmYiLAogICBzZXJpZXM6IFsKICAgICAgewogICAgICAgICB0eXBlOiAncGllJywKICAgICAgICAgcmFkaXVzOiBbJzMwJScsICczNSUnXSwKICAgICAgICAgY2VudGVyOiBbJzYwJScsICc1MCUnXSwKICAgICAgICAgbGFiZWw6IHsKICAgICAgICAgICAgc2hvdzogZmFsc2UsCiAgICAgICAgIH0sCiAgICAgICAgIGRhdGE6IGRhdGEsCiAgICAgICAgIGl0ZW1TdHlsZTogewogICAgICAgICAgICBib3JkZXJXaWR0aDogMiwKICAgICAgICAgICAgYm9yZGVyQ29sb3I6ICcjZmZmJwogICAgICAgICB9LAogICAgICAgICBtaW5BbmdsZTogMwogICAgICB9CiAgIF0sCiAgIGxlZ2VuZDogWwogICAgICB7CiAgICAgICAgIG9yaWVudDogJ3ZlcnRpY2FsJywKICAgICAgICAgbGVmdDogJzE1JScsCiAgICAgICAgIHRvcDogJ2NlbnRlcicsCiAgICAgICAgIGljb246ICdjaXJjbGUnLAogICAgICAgICBmb3JtYXR0ZXI6IChsZWcpID0+IHsKICAgICAgICAgICAgbGV0IGl0ZW0gPSBkYXRhLmZpbHRlcigoVikgPT4gVi5uYW1lID09IGxlZylbMF07CiAgICAgICAgICAgIGxldCBtYXBLID0gZGF0YU5hbWVNYXBbbGVnXTsKICAgICAgICAgICAgbGV0IGljb24gPSAnaWNvbl8nICsgbWFwSzsKICAgICAgICAgICAgbGV0IHZhbHVlMSA9ICd2YWx1ZTFfJyArIG1hcEs7CiAgICAgICAgICAgIGxldCB2YWx1ZTIgPSAndmFsdWUyXycgKyBtYXBLOwogICAgICAgICAgICBpZiAoc2VsZWN0c1tsZWddKSB7CiAgICAgICAgICAgICAgIHJldHVybiBgeyR7aWNvbn18fXt0aXRsZTF8JHtsZWd9fXt0aXRsZTJ85Y2g5q+UfVxue3poYW58fVxueyR7dmFsdWUxfXwke2l0ZW0udmFsdWV9fXske3ZhbHVlMn18JHtwdXNoWmVybyhpdGVtLnBybyl9JX1gCiAgICAgICAgICAgIH0gZWxzZSB7CiAgICAgICAgICAgICAgIC8vIGljb27ngbDoibIKICAgICAgICAgICAgICAgcmV0dXJuIGB7bnNlbHx9e3RpdGxlMXwke2xlZ319e3RpdGxlMnzljaDmr5R9XG57emhhbnx9XG57JHt2YWx1ZTF9fCR7aXRlbS52YWx1ZX19eyR7dmFsdWUyfXwke3B1c2haZXJvKGl0ZW0ucHJvKX0lfWAKCiAgICAgICAgICAgICAgIC8vIOWPlua2iOmAieaLqeeahOWFqOeBsOiJsgogICAgICAgICAgICAgICAvLyByZXR1cm4gYHtuc2VsfH17dGl0bGUxfCR7bGVnfX17dGl0bGUyfOWNoOavlH1cbnt6aGFufH1cbntudmFsdWUxfCR7aXRlbS52YWx1ZX19e252YWx1ZTJ8JHtwdXNoWmVybyhpdGVtLnBybyl9JX1gCiAgICAgICAgICAgIH0KICAgICAgICAgfSwgIC8vMTQgKyAxNCArIDE4CiAgICAgICAgIGl0ZW1XaWR0aDogMCwKICAgICAgICAgaXRlbUhlaWdodDogMCwKICAgICAgICAgcGFkZGluZzogMCwKICAgICAgICAgaXRlbUdhcDogMzAsCiAgICAgICAgIGRhdGE6IGxlZ2VuZCwKICAgICAgICAgdGV4dFN0eWxlOiB7CiAgICAgICAgICAgIHJpY2g6IGxlZ2VuZFJpY2hzCiAgICAgICAgIH0KICAgICAgfQogICBdCn07CgpteUNoYXJ0Lm9uKCdsZWdlbmRzZWxlY3RjaGFuZ2VkJywgKGV2ZW50KSA9PiB7CiAgIHNlbGVjdHMgPSBldmVudC5zZWxlY3RlZDsKICAgbXlDaGFydC5zZXRPcHRpb24oewogICAgICBsZWdlbmQ6IHt9CiAgIH0pCn0p`
let Base64 = {
_keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
encode: function (e: string) {
let t = "";
let n, r, i, s, o, u, a;
let f = 0;
e = Base64._utf8_encode(e);
while (f < e.length) {
n = e.charCodeAt(f++);
r = e.charCodeAt(f++);
i = e.charCodeAt(f++);
s = n >> 2;
o = (n & 3) << 4 | r >> 4;
u = (r & 15) << 2 | i >> 6;
a = i & 63;
if (isNaN(r)) {
u = a = 64
} else if (isNaN(i)) {
a = 64
}
t = t + this._keyStr.charAt(s) + this._keyStr.charAt(o) + this._keyStr.charAt(u) + this._keyStr.charAt(a)
}
return t
},
decode: function (e: string) {
let t = "";
let n, r, i;
let s, o, u, a;
let f = 0;
e = e.replace(/[^A-Za-z0-9+/=]/g, "");
while (f < e.length) {
s = this._keyStr.indexOf(e.charAt(f++));
o = this._keyStr.indexOf(e.charAt(f++));
u = this._keyStr.indexOf(e.charAt(f++));
a = this._keyStr.indexOf(e.charAt(f++));
n = s << 2 | o >> 4;
r = (o & 15) << 4 | u >> 2;
i = (u & 3) << 6 | a;
t = t + String.fromCharCode(n);
if (u != 64) {
t = t + String.fromCharCode(r)
}
if (a != 64) {
t = t + String.fromCharCode(i)
}
}
t = Base64._utf8_decode(t);
return t
},
_utf8_encode: function (e: string) {
e = e.replace(/rn/g, "n");
let t = "";
for (let n = 0; n < e.length; n++) {
let r = e.charCodeAt(n);
if (r < 128) {
t += String.fromCharCode(r)
} else if (r > 127 && r < 2048) {
t += String.fromCharCode(r >> 6 | 192);
t += String.fromCharCode(r & 63 | 128)
} else {
t += String.fromCharCode(r >> 12 | 224);
t += String.fromCharCode(r >> 6 & 63 | 128);
t += String.fromCharCode(r & 63 | 128)
}
}
return t
},
_utf8_decode: function (e: string) {
let t = "";
let n = 0;
let r: number = 0;
let c1: number = 0;
let c2: number = 0;
let c3 = null;
while (n < e.length) {
r = e.charCodeAt(n);
if (r < 128) {
t += String.fromCharCode(r);
n++
} else if (r > 191 && r < 224) {
c2 = e.charCodeAt(n + 1);
t += String.fromCharCode((r & 31) << 6 | c2 & 63);
n += 2
} else {
c2 = e.charCodeAt(n + 1);
c3 = e.charCodeAt(n + 2);
t += String.fromCharCode((r & 15) << 12 | (c2 & 63) << 6 | c3 & 63);
n += 3
}
}
return t
}
};
export const code: string = Base64.decode(_str);
三、预览容器
在项目根目录public文件中创建,EchartsView文件夹
1.frame.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html,
body {
height: 100%;
margin: 0px;
padding: 0px;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
.container{
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<div class="container"></div>
</body>
<script id="echartsjs" src=""></script>
<script src="./main.js"></script>
<script id="codes"></script>
</html>
2.main.js
messageData
存放postMessage传递的数据;- 通过获取
iframe
地址栏参数version
创建不同版本的cdn script
标签 - 动态将创建的
echarts cdn
标签插入,监听message
事件拿到编辑器代码 - 创建
script
标签将代码写入后,插入iframe
let messageData = {};
const handleQuery = (query) => {
let queryObj = {}
if (query) {
query.split('&').forEach((item) => {
let arr = item.split('=')
queryObj[arr[0]] = arr[1]
})
}
return queryObj
}
const URLDATA = window.location.href.split('?')[1]
const AllParams = handleQuery(URLDATA);
const setVersionEchart = () => {
let version = AllParams.version;
if (!version) return;
let echartsjs = document.querySelector("#echartsjs");
document.body.removeChild(echartsjs);
let script = document.createElement('script');
script.setAttribute('id', 'echartsjs');
let cdn = `https://cdn.staticfile.org/echarts/${version}/echarts.min.js`
script.src = cdn;
document.body.appendChild(script);
script.onload = () => {
window.addEventListener('message', res => {
console.log(res);
messageData = res.data;
initEchart()
}, false)
}
}
setVersionEchart();
const initEchart = () => {
let codes = messageData.codes;
let runCodes = `
let container = document.querySelector('.container');
myChart = echarts.init(container);
myChart.clear();
${codes}
myChart.setOption(option);
myChart.resize();
window.onresize = () =>{
myChart.resize();
}
`
const codesDom = document.querySelector('#codes');
codesDom.textContent = runCodes;
}
四、运行和版本切换
let frameContent = ref();
const handleClickRun = () => {
let Frame_:HTMLElement | null = document.querySelector("#Frame");
frameContent.value.removeChild(Frame_);
let newFrame:HTMLIFrameElement | any = document.createElement('iframe');
newFrame.src = iframeSrc.value;
newFrame.setAttribute('class', 'container-main-view-frame')
newFrame.setAttribute('id', 'Frame')
frameContent.value.appendChild(newFrame);
newFrame.onload = () => {
let codes = iediter.value.getValue();
if (codes.trim()) {
newFrame.contentWindow.postMessage({ codes }, '*')
}
}
}
const handleInitCode = async () => {
await getVersionOption()
handleClickRun();
}
let baseSrc = `/EchartsView/frame.html`
let iframeSrc = ref(baseSrc)
let version = ref();
let versionOption = ref([]);
const getVersionOption = () => {
return getEchartsLibraries().then(res => {
const data = res.data;
versionOption.value = data.versions.filter((v: string) => {
return parseInt(v) >= 3
}).reverse();
version.value = '5.5.0';
versionChange()
})
}
const versionChange = () => {
iframeSrc.value = baseSrc + '?version=' + version.value;
handleClickRun();
}