背景&介绍 :技术配置平台需要用在线代码编辑器功能,monaco editor
是一个由微软开发的代码编辑器,目前前端常用的代码编辑器工具vscode底层就是monaco-editor实现的,也就是说,我们在 vscode
里面能够做到的功能理论上你也是可以通过 monaco editor
来实现的,交互以及UI上几乎是一模一样的,有点不同的是,两者的平台不一样,monaco基于浏览器,而VSCode基于桌面应用程序electron ,Monaco Editor 有一份完整的官方文档,但是官方文档我没看懂,可能是我查阅的方式不对吧
下面开始总结我在本次开发中所用的功能,使用两个月后整理可能有些细节忘记了,有点流水账,先记录个笔记,下次迭代组件时候再来完善。
一、基本功能
安装monaco
npm install monaco-editor -S
npm install monaco-editor-webpack-plugin -S
然后在自己的文件中引入monaco,这里不需要全部引入,只需要引入自己需要使用的功能模块即可。
HTML
ini
<div id="monaco-editor" ref="monacoEditor" />
JS
javascript
// 全部引入
import * as monaco from 'monaco-editor';
// 部分引入
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
// 语法高亮
import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution';
// 采用如下这种方式引入的话,会自动带上所有的内置语言和控件,唯一的缺点就是包的体积过大。
import * as monaco from 'monaco-editor/esm/vs/editor/editor.main.js';
const monacoInstance=monaco.editor.create(document.getElementById("monaco"),{
value:`console.log("hello,world")`,
language:"javascript"
})
monacoInstance.dispose();//使用完成销毁实例
二、vue2项目中使用monaco编辑器demo如下:
三、进阶使用
提供一个定义worker路径的全局变量
ini
window.MonacoEnvironment = {
getWorkerUrl: function (moduleId, label) {
if (label === 'json') {
return './json.worker.js';
}
if (label === 'css') {
return './css.worker.js';
}
if (label === 'html') {
return './html.worker.js';
}
if (label === 'typescript' || label === 'javascript') {
return './typescript.worker.js';
}
if(label==="sql"){
return "./sql.worker.js";
}
return './editor.worker.js';
}
}
选择对应的language,monaco会去调用getWorkerUrl去查worker的路径,然后去加载。这边默认会加载一个editor.worker.js,这是一个基础功能文件,提供了所有语言通用的功能(例如已定义常量的代码补全提示),无论使用什么语言,monaco都会去加载他。
四、打包worker
注意:插件和webpack等版本注意:monaco-editor与monaco-editor-webpack-plugin的版本要兼容
perl
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
"monaco-editor-webpack-plugin": "^2.0.0",
"monaco-editor": "^0.21.2",
"@zero/zero-cli": "^0.0.7-30",
webpack注意:
css
{
test: /\.ttf$/,
loader: 'file-loader',
options: {
name: "cmdicon.ttf"
}
},
publicPath: process.env.WEBPACK_DEV_SERVER == 'true' ? 'http://127.0.0.1:9999/' : 'https://XXX.XXX.net/v1/mss_0ddf9b250a1b4db28fb1d9ad764b2853/files/monaco/',
path: path.resolve('./') + '/dist',
filename: 'ai_[name]_bundle.js',
libraryTarget: 'umd',
umdNamedDefine: true
打包过程中遇见了很多问题
css
entry: {
"main": path.resolve(process.cwd(), "src/main.js"),
"editor.worker": 'monaco-editor/esm/vs/editor/editor.worker.js',
"ts.worker": 'monaco-editor/esm/vs/language/typescript/ts.worker',
},
问题一:版本要问题,版本要一一对应
问题二:我们的输出一般是加hash的,所以,输出的worker文件也会有对应的hash值后缀,例如typescript.worker.a23sf4asfqw.js,那么,第一步中的getWorkerUrl中的配置(typescript.worker.js)就和他对不上了,导致查找worker路径失败。
问题三:worker是运行在单独的线程中的,所以没有window变量,我们需要修改webpack的全局变量为self才可以。
不得不说monaco是一个很贴心的编辑器,他也帮我们解决了这一系列问题。解决我们问题的就是**monaco-editor-webpack-plugin。**
插件会帮我们做这么几件事
- 自动注入getWorkerUrl全局变量
- 处理worker的编译配置
- 自动引入控件和语言包。
arduino
new MonacoWebpackPlugin({
languages:["javascript","css","html","json"],
features:["coreCommands","find"]
})
事件绑定
在完成了编辑器本身的配置之后,我们可以开始进行下一步,绑定编辑事件。
javascript
monacoInstance.onDidChangeModelContent((event) => {
const newValue=monacoInstance.getValue();
console.log(newValue)
})
monacoInstance 是一个create 方法返回的实例,他包含很多操作实例的方法。event 是一个IModelContentChangedEvent对象,他包含了非常非常详细的变更信息,包括操作的类型(撤销、恢复,还是手动输入引发的文本变更),变更的文本位置,变更的文本内容等。而我们要获取最新的值,则需要调用
abnf
monacoInstance.getValue();
细心的朋友应该还会发现一个很奇怪的地方,那就是我们绑定的方法用的是onDidChangeModelContent,里面有一个Model,这命名可是很讲究的,字面意思就是**变更Model内容触发事件,**从头到尾,我们都没看到有Model的存在,那么为什么这边是变更Model内容触发事件呢,难道我们操作的是Model?
是的,其实我们在编辑的时候,就是在Model上编辑,默认情况下,monaco会帮我生成一个Model,我们可以调用getModel打印一下
reasonml
monacoInstance.getModel()
看一看api,我们可以发现,Model其实是一个保存编辑状态的对象,他里面含有语言信息,当前的编辑文本信息,标注信息等。所以我们可以缓存一下Model对象,在需要的时候直接调用setModel即可随时切换到之前的状态。或者也可以在初始化实例的时候设置一个Model。
ini
const model=monaco.editor.createModel("hahahaha","javascript");
monacoInstance = monaco.editor.create(this.monacoDom.current, {
model:model
})
而且我们可以直接在model上来绑定我们的事件
javascript
model.onDidChangeContent((event)=>{
...
})
Model最后也需要我们销毁,这里分两种情况,假如是通过createModel创建的Model,那么我们需要手动销毁,但是如果是monaco默认创建的,则不需要,在调用实例的销毁方法时,会顺带销毁默认创建的Model。
abnf
model.dispose();
除了编辑事件之外,Model还有许多其他事件
例如:
onDidChangeOptions 配置改变事件
onDidChangeLanguage 语言改变事件
...
在简单的场景下,Model的存在可能使得我们使用起来比较繁琐,但是,在复杂场景下,model可以极大的简化代码复杂性。
设想一下我们有5个tab,每个tab都是一个编辑器,每个编辑器都有各自的语言,内容和标注信息,如果没有Model,我们需要保存每个tab的语言,内容等信息,在切换到对应tab时再将这些信息初始化到编辑器上,但是利用Model,我们不需要额外去保存每个tab的编辑信息,只需要保存每个tab的Model,然后将Model传给编辑器进行初始化即可。
我的webpack
javascript
const path = require('path');
const fs = require('fs');
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const processDir = process.env.dir;
// monaco-editor必备
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
console.log(11,process.env.WEBPACK_DEV_SERVER,path.resolve('./') + '/dist','ai_[name]_bundle.js')
module.exports = [{
entry: {
sub: path.resolve('./') + '/index.js'
},
// output: {
// path: path.resolve('./') + '/dist',
// filename: '[name]_bundle.js',
// libraryTarget: 'umd',
// umdNamedDefine: true
// },
output: {
publicPath: process.env.WEBPACK_DEV_SERVER == 'true' ? 'http://127.0.0.1:9999/' : 'https://XXXXXX.XXX.net/v1/mss_0ddf9b250a1b4db28fb1d9ad764b2853/files/monaco/',
path: path.resolve('./') + '/dist',
filename: 'ai_[name]_bundle.js',// monaco-editor必备
libraryTarget: 'umd',
umdNamedDefine: true
},
resolve: {
extensions: ['.js', '.vue', '.ttf']
},
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
// {
// test: /\.ttf$/,
// use: ['file-loader'],
// },
// 必备
{
test: /\.ttf$/,
loader: 'file-loader',
options: {
name: "cmdicon.ttf"
}
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
js: {
loader: "babel-loader",
options: {
presets: ["es2015", "stage-0", "env"],
plugins: ["@babel/transform-runtime", "transform-vue-jsx", "transform-object-rest-spread"],
comments: false,
}
}
},
}
},
{
test: /\.(js|jsx|es6)$/,
loader: 'babel-loader?cacheDirectory',
include: [path.resolve(__dirname, `./components/${processDir}`)],
exclude: /node_modules/,
options: {
presets: ['@babel/preset-env', "@vue/babel-preset-jsx"],
}
},
{
test: /\.css$/,
loader: 'style-loader!css-loader'
}
]
},
optimization: {
splitChunks: {
cacheGroups: {
monacoEditor: {
chunks: 'async',
name: 'zero_monaco',
priority: 22,
test: /[\/]node_modules[\/]monaco-editor[\/]/,
enforce: true,
reuseExistingChunk: true,
}
}
},
},
plugins: [
new MonacoWebpackPlugin() ,// monaco-editor必备
new VueLoaderPlugin(),
]
}]
五、按需导入&按需引入语言
六、常用API
editor.getValue()
获取编辑器中的所有文本,并生成一个字符串返回,会保留所有信息(换行、缩进、注释等等)。
editor.getSelection()
获取编辑器中被选中文案的 range ,返回一个对象,如下:
javascript
{
startLineNumber: 0,
startColumnNumber: 0,
endLineNumber: 0,
endColumnNumber: 0,
}
editor.getModel()
获取编辑器当前的 textmodel,一般不会直接使用,通过 textmodel 可以对文本各种操作。
editor.getModel().findMatches(str|regexp)
功能和 "⌘ + F" 一致,通过字符串或正则表达式查找编辑器内匹配的文本,并返回匹配文本 range 的集合。
editor.getModel().getValueInRange(range)
通过 range 获取范围内的文本,返回一个字符串。
editor.getModel().getLinesContent(lineNumber)
如果传入 lineNumber,则返回对应行的文本字符串,不传参则返回所有行的文本字符串的集合。
editor.executeEdits()
在指定位置插入代码,跟 editor.setValue() 不同的地方是,可以用 "⌘ + Z" 撤销输入。
javascript
editor.executeEdits('insert-code', [
{
range: {
startLineNumber,
startColumn,
endLineNumber,
endColumn,
},
text,
},
])
editor.addAction()
在右键菜单里增加一栏自定义的操作。
javascript
this.editor.addAction({
id: '', // 菜单项 id
label: '', // 菜单项名称
keybindings: [this.monaco.KeyMod.CtrlCmd | this.monaco.KeyCode.KEY_J], // 绑定快捷键
contextMenuGroupId: '9_cutcopypaste', // 所属菜单的分组
run: () => {}, // 点击后执行的操作
})
monaco.editor.setModelMarkers()
在编辑器中用波浪线标出错误提示。
javascript
monaco.editor.setModelMarkers(editor.getModel(), 'owner', [
{
startLineNumber,
startColumn,
endLineNumber,
endColumn,
message, // 提示文案
severity: monaco.MarkerSeverity.Error, // 提示的类型
},
])
七、monaco-editor得配置参数详解
json
{
"acceptSuggestionOnCommitCharacter": true, // 接受关于提交字符的建议
"acceptSuggestionOnEnter": "off", // 接受输入建议 "on" | "off" | "smart"
"accessibilityPageSize": 10, // 辅助功能页面大小 Number 说明:控制编辑器中可由屏幕阅读器读出的行数。警告:这对大于默认值的数字具有性能含义。
"accessibilitySupport": "on", // 辅助功能支持 控制编辑器是否应在为屏幕阅读器优化的模式下运行。
"autoClosingBrackets": "always", // 是否自动添加结束括号(包括中括号) "always" | "languageDefined" | "beforeWhitespace" | "never"
"autoClosingDelete": "always", // 是否自动删除结束括号(包括中括号) "always" | "never" | "auto"
"autoClosingOvertype": "always", // 是否关闭改写 即使用insert模式时是覆盖后面的文字还是不覆盖后面的文字 "always" | "never" | "auto"
"autoClosingQuotes": "always", // 是否自动添加结束的单引号 双引号 "always" | "languageDefined" | "beforeWhitespace" | "never"
"automaticLayout": true, // 自动布局
"codeLens": false, // 是否显示codeLens 通过 CodeLens,你可以在专注于工作的同时了解代码所发生的情况 -- 而无需离开编辑器。 可以查找代码引用、代码更改、关联的 Bug、工作项、代码评审和单元测试。
"codeLensFontFamily": '', // codeLens的字体样式
"codeLensFontSize": 13, // codeLens的字体大小
"colorDecorators": false, // 呈现内联色彩装饰器和颜色选择器
"comments": {
"ignoreEmptyLines": true, // 插入行注释时忽略空行。默认为真。
"insertSpace": true, // 在行注释标记之后和块注释标记内插入一个空格。默认为真。
}, // 注释配置
"contextmenu": false, // 启用上下文菜单
"columnSelection": false, // 启用列编辑 按下shift键位然后按↑↓键位可以实现列选择 然后实现列编辑
"autoSurround": "never", // 是否应自动环绕选择
"copyWithSyntaxHighlighting": true, // 是否应将语法突出显示复制到剪贴板中 即 当你复制到word中是否保持文字高亮颜色
"cursorBlinking": "smooth", // 光标动画样式
"cursorSmoothCaretAnimation": "on", // 是否启用光标平滑插入动画 当你在快速输入文字的时候 光标是直接平滑的移动还是直接"闪现"到当前文字所处位置
"cursorStyle": "line", // "Block"|"BlockOutline"|"Line"|"LineThin"|"Underline"|"UnderlineThin" 光标样式
"cursorSurroundingLines": 0, // 光标环绕行数 当文字输入超过屏幕时 可以看见右侧滚动条中光标所处位置是在滚动条中间还是顶部还是底部 即光标环绕行数 环绕行数越大 光标在滚动条中位置越居中
"cursorSurroundingLinesStyle": "all", // "default" | "all" 光标环绕样式
"cursorWidth": 2, // <=25 光标宽度
"minimap": {
"enabled": false, // 是否启用预览图
}, // 预览图设置
"scrollbar": {
"verticalScrollbarSize": 5,
"horizontalScrollbarSize": 5,
"arrowSize": 10,
"alwaysConsumeMouseWheel": false,
},
"folding": false, // 是否启用代码折叠
"links": true, // 是否点击链接
"overviewRulerBorder": false, // 是否应围绕概览标尺绘制边框
"renderLineHighlight": "gutter", // 当前行突出显示方式
"scrollBeyondLastLine": false, // 设置编辑器是否可以滚动到最后一行之后
"readOnly": false, // 是否为只读模式
"lineNumbers": "on",
"lineNumbersMinChars": 0,
"theme": "vs", //官方自带三种主题vs, hc-black, or vs-dark
"value": "", //编辑器初始显示文字
"language": "json",
"fontSize": 13,
"roundedSelection": true, // 右侧不显示编辑器预览框
"autoIndent": "full",
"formatOnType": true,
"formatOnPaste": true
}
八、在线代码编辑器组件源码demo
index.vue
vue
<template>
<div v-bind="$attrs">
<div id="monaco-editor" ref="monacoEditor" />
</div>
</template>
<script>
import * as monaco from 'monaco-editor';
import debounce from 'lodash/debounce';
// import * as monaco from "monaco-editor/esm/vs/editor/editor.main";
export default {
name: "FControlMonacoEditor",
props: {
data: {
type: Array,
default: () => [],
},
// 编辑器支持的文本格式
types: {
type: String,
default:"json"
},
// 名称
name: {
type: String,
default:"test"
},
editorOptions: {
type: Object,
default: {
selectOnLineNumbers: true,
roundedSelection: false,
readOnly: false, // 只读
writeOnly: false,
cursorStyle: "line", //光标样式
automaticLayout: true, //自动布局
glyphMargin: true, //字形边缘
useTabStops: false,
fontSize: 32, //字体大小
autoIndent: true, //自动布局
//quickSuggestionsDelay: 500, //代码提示延时
},
},
defaultCodes: {
type: String,
default: function () {
return "";
},
},
},
data() {
return {
editor: null, //文本编辑器
isSave: true, //文件改动状态,是否保存
codeValue: "", //保存后的文本
codesCopy:null,
};
},
watch: {
defaultCodes: function (newValue) {
if (this.editor) {
if (newValue !== this.editor.getValue()) {
this.editor.setValue(newValue);
this.editor.trigger(this.editor.getValue(), 'editor.action.formatDocument')
}
}
}
},
mounted() {
this.renderMonaco();
},
beforeDestroy() {
this.editor.dispose();
},
methods: {
renderMonaco() {
// 初始化container的内容,销毁之前生成的编辑器
if (this.$refs.monacoEditor) {
const self = this;
this.$refs.monacoEditor.innerHTML = "";
// 初始化编辑器,确保dom已经渲染
this.editor = monaco.editor.create(self.$refs.monacoEditor, {
value: self.codeValue || self.defaultCodes || '', // 编辑器初始显示内容
language: self.types, // 支持语言
theme: 'vs-dark', // 'myStyle'
selectOnLineNumbers: true, //显示行号
editorOptions: function () {
return self.editorOptions
}
});
// 编辑器内容发生改变时触发
self.$emit("onMounted", self.editor); //编辑器创建完成回调
self.editor.onDidChangeModelContent(debounce(() => {
//编辑器内容changge事件
self.codeValue = self.editor.getValue();
self.$emit("onCodeChange", self.editor.getValue(), event);
}, 500));
}
},
// 获MonacoEditor的value值
getMonacoEditorVal({ data, callback, event }) {
// return this.editor.getValue();
this.codeValue =this.editor.getValue();
callback({
data: Object.assign({}, data, { [this.name]: this.editor.getValue() }),
event,
});
},
initData({ data, callback, event }) {
if (!data) return;
const info = data.data || data;
this.codeValue = info;
this.renderMonaco();
callback && callback({
data,
event
})
},
}
};
</script>
<style scoped>
#monaco-editor {
width: 100%;
height: 300px;
overflow-y: auto;
}
</style>
config.js组件属性设置
javascript
module.exports = {
pkgVersion: "0.0.1",
type: 0,
layoutType: null,
slots: null,
props: null,
schemas: [
{
name: "在线代码编辑器",
componentName: "FControlMonacoEditor",
props: [
{
desc: {
propName: "types",
label: "编辑器支持的文本格式",
componentName: "mtd-input",
type: "string"
},
config: {
attrs: {},
default: "json",
},
},
{
desc: {
propName: "name",
label: "名称",
componentName: "mtd-input",
type: "string"
},
config: {
attrs: {},
default:"test",
},
},
{
desc: {
propName: "editorOptions",
label: "代码编辑器配置",
componentName: 3712,
type: "Object"
},
config: {
attrs: {
rows: 6
},
default: {
selectOnLineNumbers: true,
roundedSelection: false,
readOnly: false, // 只读
writeOnly: false,
cursorStyle: "line", //光标样式
automaticLayout: true, //自动布局
glyphMargin: true, //字形边缘
useTabStops: false,
fontSize: 32, //字体大小
autoIndent: true, //自动布局
//quickSuggestionsDelay: 500, //代码提示延时
},
visible: {},
description: "代码编辑器配置"
},
},
{
desc: {
propName: "defaultCodes",
label: "编辑器初始化默认code值",
componentName: "mtd-textarea",
type: "string"
},
config: {
attrs: {
rows: "5",
placeholder: "请输入编辑器初始化默认给定的提示模版",
clearable: true
},
default: "class",
},
},
],
resources: {
3712: {
},
},
actions: {
mounted: { name: "mounted", description: "挂载完成" },
btnClick: { name: "btnClick", description: "点击操作按钮" }
},
events: {
initData: {
name: "initData",
description: "初始化数据"
},
getMonacoEditorVal:{
name: "getMonacoEditorVal",
description: "获MonacoEditor的value值"
}
},
},
],
};
九、在线编辑器js语法版本demo
codeEditor.vue
vue
<template>
<mtd-drawer
v-model="show"
width="70%"
class="code-drawer"
:closable="false"
:mask-closable="false"
:title="title"
>
<div class="handle-action">
<fun-search v-if="this.options.language !== 'css'" ref="funSearch" @close-modal="closeModal()"></fun-search>
<mtd-popover
trigger="click"
v-if="options.language !== 'css'"
placement="bottom"
>
<mtd-button type="text-primary">内置函数使用示例</mtd-button>
<div slot="title">内置函数使用示例</div>
<div slot="content" class="monaco-popover-content">
<div v-html="funExample" style="white-space: pre-wrap; font-size: 16px;color: #d4d4d4;"></div>
</div>
</mtd-popover>
<mtd-popover
trigger="click"
v-if="options.language !== 'css'"
placement="bottom"
>
<mtd-icon-button icon="mtdicon mtdicon-question-circle-o" />
<div slot="title">编写说明</div>
<div slot="content" class="monaco-popover-content">
<div v-html="defaultCode" style="white-space: pre-wrap"></div>
</div>
</mtd-popover>
<mtd-icon-button
class="demo-icon-btn"
icon="mtdicon mtdicon-save-o"
@click="saveCode('save')"
/>
<mtd-icon-button
class="demo-icon-btn"
icon="mtdicon mtdicon-close"
@click="cancelCode"
/>
</div>
<div class="code-editor">
<div ref="codeContainer" class="code-editor-container"></div>
<mtd-popover trigger="hover" placement="top">
<div v-show="errorInfo.message" class="error-tips" @click="cursorPostion(errorInfo)">{{errorInfo.message}}</div>
<div slot="content" class="demo-popover-content">
点击定位到错误位置
</div>
</mtd-popover>
</div>
</mtd-drawer>
</template>
<script>
import * as monaco from "monaco-editor";
import { DEFAULT_CODE, DEFAULT_CODE_Style, FUN_EXAMPLE, linterConfig } from "./constant";
import FunSearch from './funSearch';
import { Linter } from '@zero/zero-eslint'
import debounce from 'lodash/debounce';
export default {
name: "codeEditor",
components: {
FunSearch
},
props: {
show: Boolean,
id: String,
value: {
type: String,
default: "",
},
options: {
type: Object,
default() {
return {};
},
},
},
watch: {
show(val) {
if (!this.isOpenDrawer && val) {
this.$nextTick(() => {
this.initEditor();
});
this.isOpenDrawer = true;
}
},
codes: {
handler(val) {
this.initEditor();
},
},
options: {
deep: true,
handler(newVal) {
if (this.editor) {
monaco.editor.setModelLanguage(
this.editor.getModel(),
newVal.language || "javascript"
);
this.editor.setValue(this.value);
}
//更改说明
this.defaultCode =
newVal.language === "javascript" ? DEFAULT_CODE : DEFAULT_CODE_Style;
},
},
},
data() {
return {
isOpenDrawer: false,
editor: null,
defaultCode: DEFAULT_CODE,
funExample: FUN_EXAMPLE,
// 主要配置
defaultOpts: {
value: "", // 编辑器的值
language: "javascript", // shell、sql、python
theme: "vs-dark", // 编辑器主题:vs, hc-black, or vs-dark,更多选择详见官网
roundedSelection: false, // 右侧不显示编辑器预览框
autoIndent: true, // 自动缩进
readOnly: false, // 不能编辑
fontSize: "14px"
},
linter: '',
errorInfo: {
message: '',
line: '',
endColumn: '',
},
};
},
created() {
this.linter = new Linter();
},
computed: {
title() {
return this.options.language == "css" ? "编辑样式" : "编辑可执行方法";
},
},
methods: {
closeModal() {
this.cancelCode(true);
},
initEditor() {
// 初始化container的内容,销毁之前生成的编辑器
if (this.$refs.codeContainer && !this.editor) {
const _self = this;
this.$refs.codeContainer.innerHTML = "";
this.editorOptions = Object.assign(this.defaultOpts, this.options, {
value: this.value,
});
// 生成编辑器对象
this.editor = monaco.editor.create(
this.$refs.codeContainer,
this.editorOptions
);
const model = this.editor.getModel();
// 编辑器内容发生改变时触发
this.editor.onDidChangeModelContent(debounce(() => {
this.$emit("contentChange", this.editor.getValue());
if(this.options.language === 'javascript') {
this.verify();
}
}, 500));
// 添加保存快捷键
this.editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
() => {
_self.saveCode("ctrl-save");
}
);
}
},
// 编辑器光标定位到匹配行
findMatches(str) {
if (!str) return;
if (!this.editor) this.initEditor();
this.editor.getModel().setValue(this.value);
const matches = this.editor
.getModel()
.findMatches(str, true, false, false, null, true);
console.log("matches", matches);
const { startLineNumber = 0, endColumn = 0 } = matches[0]
? matches[0].range
: {};
this.$nextTick(() => {
// 滚动到特定行,使其在编辑器的中心结束
this.editor.revealLineInCenter(startLineNumber);
this.editor.focus();
this.editor.setPosition({
column: endColumn,
lineNumber: startLineNumber,
});
});
},
cancelCode(noTips = false) {
this.reset();
this.$emit("cancel", {noTips, value: this.editor && this.editor.getValue()});
},
reset() {
if(this.$refs.funSearch) {
this.$refs.funSearch.value = '';
}
this.errorInfo = {
message: '',
line: '',
endColumn: '',
};
},
// 光标定位
cursorPostion(positionInfo) {
const { line, endColumn } = positionInfo;
this.$nextTick(() => {
this.editor.revealLineInCenter(line);
this.editor.focus();
this.editor.setPosition({
column: endColumn,
lineNumber: line,
});
});
},
// 本地保存
saveCode(type) {
const model = this.editor.getModel();
if(this.options.language === 'javascript') {
this.verify(model);
const { message, line, endColumn } = this.errorInfo;
if(message) {
this.$mtd.confirm({
title: '警告提示',
message: `当前的代码解析出错,代码内容将无法保存,请重新编辑后关闭面板以保存。\n${message}`,
width: '430px',
type: 'warning',
showCancelButton: false,
okButtonText: '定位到错误位置',
onOk: () => {
this.cursorPostion(this.errorInfo);
},
}).catch(() => {});
return;
}
}
const _self = this;
const value = this.getVal();
if (value) {
this.saveSuccess(type);
} else {
const h = this.$createElement;
const n = this.$mtd.notify({
type: "warning",
title: "提示确认",
duration: 0,
offset:200,
message: h("div", {}, [
h("div", "当前内容为空"),
h("mtd-button", {
domProps: {
innerHTML: "保存",
},
style: {
marginTop: "8px",
float: "right",
},
props: {
type: "primary",
size: "small",
},
on: {
click() {
n.close();
_self.saveSuccess(type);
},
},
}),
]),
});
}
},
saveSuccess(type) {
this.$mtd.notify({
type: "success",
title: "成功",
message: `当前内容已保存`,
duration: 700,
offset:200,
});
this.reset();
this.$emit(type, this.getVal());
},
// 供父组件调用手动获取值
getVal() {
return this.editor.getValue();
},
verify() {
const model = this.editor.getModel();
const value = model.getValue();
const errs = this.linter.verify(value, linterConfig);
const severityMap = {
2: 8,
1: 4,
};
const ruleDefines = this.linter.getRules();
const lineNode = document.querySelector(".margin-view-overlays")
const errList = errs.filter(val => val.severity === 2);
if(errList.length > 0) {
const cur = errList[0];
this.errorInfo.message = `${cur.message}(${cur.line}:${cur.column})`
this.errorInfo.line = cur.line;
this.errorInfo.endColumn = cur.endColumn;
} else {
this.errorInfo = {
message: '',
line: '',
endColumn: '',
};
}
const markers = errs.map((err) => {
const o = err.ruleId ? ruleDefines.get(err.ruleId) : '';
const target = o && o.meta && o.meta.docs && o.meta.url ? o.meta.url : '';
return {
code: {
value: err.ruleId,
target,
},
startLineNumber: err.line,
endLineNumber: err.endLine,
startColumn: err.column,
endColumn: err.endColumn,
message: err.message,
severity: severityMap[err.severity],
source: "eslint",
}
});
monaco.editor.setModelMarkers(model, "eslint", markers);
},
},
};
</script>
<style lang="scss" scope>
.code-drawer {
.mtd-drawer-close {
left: 16px;
right: auto;
}
.code-editor {
position: fixed;
/* height: 100%; */
height: calc(100% - 80px);
width: calc(100% - 48px);
.code-editor-container {
width: 100%;
height: 100%;
.monaco-editor .scroll-decoration {
box-shadow: none;
}
}
}
.handle-action {
position: fixed;
top: 16px;
right: 17px;
text-align: right;
z-index: 2;
display: flex;
button + button {
margin-left: 0px !important;
}
}
}
.monaco-popover-content {
height: 600px;
padding-left: 20px;
color: #ffffff;
background-color: #1e1e1e;
overflow-y: scroll;
}
.error-tips{
width: 70%;
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(255,234,234,.8);
color: red;
padding: 0 12px;
margin: 2px 0 0;
overflow: hidden;
line-height: 22px;
height: 22px;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
}
</style>
funSearch.vue
vue
<template>
<div class="fun-search">
<mtd-input v-model="value" placeholder="输入函数名查找使用位置" clearable style="width: 230px"></mtd-input>
<div v-if="funSourceList.length > 0" class="demo-box">
<mtd-card class="card-box" body-class="card-body-cover">
<mtd-list>
<mtd-list-item v-for="(item, key) in funSourceList"
:key="key"
@click=jumpComponentConfirm(item)
:style="{
backgroundColor: typeShowObj[item.modal].bgColor,
cursor: 'pointer'
}">
<div class="demo-content">
<div class="demo-content-main">
<div class="demo-content-title" :style="{
color: typeShowObj[item.modal].textColor
}">
{{ item.name ? `【${item.name}】` : '' }}{{ item.type }}
<span class="demo-content-description">{{ item.id }}</span>
</div>
</div>
<div class="demo-extra" :style="{
color: typeShowObj[item.modal].textColor
}">{{ typeShowObj[item.modal].title }}</div>
</div>
</mtd-list-item>
</mtd-list>
</mtd-card>
</div>
</div>
</template>
<script>
import debounce from "lodash/debounce";
import { FUN_OBJ } from './constant.js'
export default {
inject: {
zero: {
default: () => ({}),
},
},
data() {
return {
value: "",
funSourceList: [],
typeShowObj: {
dataSource: {
bgColor: "#eff8ff", // 蓝
textColor: "#042866",
title: "数据源",
sourceModalName: '数据源'
},
events: {
bgColor: "#edfaf3", // 绿
textColor: "#005238",
title: "事件编排",
sourceModalName: '事件编排'
},
formRule: {
bgColor: "#fff9e6", // 黄
textColor: "#592d00",
title: "校验规则",
sourceModalName: '规则校验设置'
},
conditionRule: {
bgColor: "#fff2f0", // 红
textColor: "#660e16",
title: "条件规则",
sourceModalName: '条件规则'
},
componentProps: {
bgColor: "#DFC6F7", // 紫
textColor: "#541cac",
title: "组件属性",
sourceModalName: '组件属性'
},
ohter: {
bgColor: "#fff2f0",
textColor: "#660e16",
title: "未知",
},
noData: {
bgColor: "#f5f5f5", // 灰
textColor: "#464646",
title: "",
},
},
};
},
watch: {
value(e) {
if (e) {
this.searchSource();
} else {
this.funSourceList = [];
}
},
},
computed: {
apiConfigList() {
return this.zero.sdk.editorConfig.apiConfigList || [];
},
nodeList() {
const events = this.zero.sdk.editorConfig.events || {};
return events.nodes || [];
},
edgeList() {
const events = this.zero.sdk.editorConfig.events || {};
return events.edges || [];
},
childrenList() {
return this.zero.sdk.editorConfig.children || [];
},
resourcesNameMap() {
const resources = this.zero.sdk.editorConfig.resources || {};
const obj = {};
Object.values(resources).forEach((val) => {
obj[val.name] = val.id;
});
return obj;
},
},
methods: {
jumpComponentConfirm(item) {
if(item.modal == 'ohter' || item.modal == 'noData' || !this.typeShowObj[item.modal].sourceModalName) {
return;
}
this.$mtd.confirm({
title: '跳转确认',
message: `确认后将不保存改动并关闭函数弹窗,跳转到引用【${this.value}】函数的${this.typeShowObj[item.modal].sourceModalName}上,确认跳转么?`,
width: '430px',
showCancelButton: true,
onOk: () => {
this.jumpComponent(item)
},
});
},
jumpComponent(item) {
this.$emit('close-modal');
if(item.modal === 'dataSource') {
this.zero.dataSourceSetting(item)
} else if(item.modal === 'events'){
this.zero.$refs.eventFlowsPanel.modalOpen(item)
} else if(item.modal === 'formRule'){
this.zero.sdk.selectNode(item.id);
this.$nextTick(() => {
FunEvent.emit('handleFormRuleShow');
})
} else if(item.modal === "conditionRule") {
this.zero.sdk.selectNode(item.id);
this.$nextTick(() => {
this.zero.$refs.configPanel.activeTab = 'eventsConfig';
})
} else if(item.modal === "componentProps"){
this.zero.sdk.selectNode(item.id);
this.$nextTick(() => {
this.zero.$refs.configPanel.activeTab = 'attrs';
})
}
},
searchSource: debounce(function () {
const sourceList = [];
// 数据源
this.apiConfigList.forEach((val) => {
const obj = {
modal: "dataSource",
name: val.name,
id: val.id
};
if (val.requestTransform === this.value) {
sourceList.push({
...obj,
type: FUN_OBJ['requestTransform'].name,
});
}
if (val.responseTransform === this.value) {
sourceList.push({
...obj,
type: FUN_OBJ['responseTransform'].name,
});
}
});
// 事件编排 请求 参数处理/响应处理
this.nodeList.forEach((val) => {
if (val.apiParams && val.apiParams.apiConfigFromGlobal === 2) {
const obj = {
modal: "events",
name: val.label,
id: val.uid,
};
if (val.apiParams.requestTransform === this.value) {
const str = "";
sourceList.push({
...obj,
type: FUN_OBJ['requestTransform'].name,
});
}
if (val.apiParams.responseTransform === this.value) {
const str = "";
sourceList.push({
...obj,
type: FUN_OBJ['responseTransform'].name,
});
}
}
});
// 事件编排 自定义函数处理
this.edgeList.forEach((val) => {
if (val.filter && val.filter.customTransforms === this.value) {
const obj = {
modal: "events",
id: val.target,
};
const label = this.getEventsLabelFromUid(val.target);
sourceList.push({
...obj,
name: label,
type: FUN_OBJ['customTransforms'].name,
});
}
});
// 校验规则
this.childrenListLoop(this.childrenList, sourceList);
if(sourceList.length === 0) {
sourceList.push({
modal: 'noData',
name: '',
type: "未找到数据",
});
}
this.funSourceList = sourceList;
}, 300),
judgeType(e) {
return Object.prototype.toString.call(e).slice(8, -1).toLowerCase();
},
childrenListLoop(arr, sourceList) {
arr.forEach((val) => {
if(val.props && val.props.conditionTransforms && val.props.conditionTransforms === this.value) {
sourceList.push({
modal: "conditionRule",
name: val.label,
type: val.props && val.props.prop,
id: val.id,
});
}
if(val.props) {
Object.keys(val.props).forEach((key) => {
// 过滤特殊属性 例如:conditionTransforms 条件规则函数字段
if(val.props[key] === this.value && key !== 'conditionTransforms') {
sourceList.push({
modal: "componentProps",
name: val.label,
type: val.props && val.props.prop,
id: val.id,
});
}
})
}
if(val.props && val.props.rules) {
const rules = val.props.rules;
if(this.judgeType(rules) === 'array' && rules.length > 0) {
rules.forEach((v) => {
if (v.validator === this.value) {
sourceList.push({
modal: "formRule",
name: val.label,
type: val.props && val.props.prop,
id: val.id
});
}
});
}
if(this.judgeType(rules) === 'object' && JSON.stringify(rules) !== "{}") {
Object.keys(rules).forEach((k) => {
if (rules[k].length > 0) {
rules[k].forEach((v) => {
if (v.validator === this.value) {
sourceList.push({
modal: "formRule",
name: val.label,
type: k,
id: val.id
});
}
});
}
});
}
}
if(val.children && val.children.length > 0) {
this.childrenListLoop(val.children, sourceList);
}
});
},
getEventsLabelFromUid(uid) {
const obj = this.nodeList.find((val) => val.uid === uid);
return obj ? obj.label : "";
},
},
};
</script>
<style lang="scss" scoped>
.fun-search {
position: relative;
margin-right: 200px;
}
.card-body-cover {
padding: 0px;
}
.card-box {
width: 400px;
text-align: left;
.mtd-list {
border: none;
}
.demo-content {
display: flex;
}
.demo-content-main {
flex: 1 0;
// margin-left: 16px;
}
.demo-more {
text-align: center;
margin: 9px 0;
cursor: pointer;
color: #4e73ff;
}
.demo-content-title {
font-weight: 500;
color: #464646;
font-size: 14px;
}
.demo-content-description {
font-size: 12px;
color: #adadad;
}
.demo-extra {
font-size: 12px;
color: #adadad;
}
}
</style>
<style lang="scss">
.fun-search {
.mtd-card-body{
padding: 0px;
}
}
</style>
constant.js
javascript
// 函数类型映射函数名称/模版
export const FUN_OBJ = {
requestTransform: {
name: '参数处理',
template: 'setParams',
},
responseTransform: {
name: '响应处理',
template: 'setResponse',
},
customTransforms: {
name: '自定义方法处理',
template: 'setCustom',
},
validator: {
name: '校验规则',
template: 'setValidator',
},
contentFunction: {
name: '自定义函数设置弹窗内容',
template: 'setConfirmContent',
},
intervalTransform: {
name: '设置轮询终止条件',
template: 'setCustom',
},
setRowColSpan: {
name: 'table合并行合并列的计算方法',
template: 'setRowColSpan',
},
indexOfSelection: {
name: 'table index-of-selection',
template: 'indexOfSelection',
},
formatCustom: {
name: 'format-data组件 自定义函数',
template: 'formatCustom',
},
tableFunction: {
name: 'table tableFunction',
template: 'tableFunction',
},
conditionTransforms: {
name: '条件规则函数处理',
template: 'setCondition',
},
}
// 编辑器初始值
export const DEFAULT_CODE = [
'/**',
'* 普通方法:编辑器中被其他方法调用',
'*/',
'func1 = (val) => {',
' console.log("you can do anything!");',
' return Number(val)',
'}',
'func2 = () => {',
' console.log("you can do anything too!");',
' return []',
'}',
'/**',
'* 参数构造方法,对输入的对象进行处理并包装',
'* 请求前调用',
'* @param { object } storedData 全局store对象,可以获取页面所有组件当前值',
'* @param { object } sourceConfig 调用该方法的组件的数据源配置',
'* @param { object } globalParams 全局参数存储对象',
'* @param object this 当前render',
'* @returns { object } 被包装后的 params 对象',
'*/',
'exports.handleRequestParams = ({ storedData, sourceConfig, globalParams }, self) => {',
' // 可以调用内部普通方法',
' const { filter = {} } = storedData',
' const result = func1(filter.org.orgType)',
' const params = { orgType : result }',
' console.log("params Handled!");',
' return params',
'}',
'/**',
'* 数据处理方法,请求数据返回后对返回结果进行处理并包装',
'* 请求后调用',
'* @param { object } resData 源数据,请求返回结果/上一个方法的数据return',
'* @param { object } resParams 请求的参数',
'* @param { object } storedData 全局store对象,可以获取页面所有组件当前值',
'* @param { object } sourceConfig 调用该方法的组件的数据源配置',
'* @param { object } globalParams 全局参数存储对象',
'* @param object this 当前render',
'* @returns { object } 被包装后的 result 对象(组件的接收数据)',
'*/',
'exports.handleResponseData = ({ resData, resParams }, self) => {',
' // 可以调用内部普通方法',
' const result = func2()',
' console.log("data Handled!");',
' return {',
' data : {',
' data : result,',
' resParams : resParams',
' }',
' }',
'}',
'/**',
'* 数据处理方法,事件触发后对输入的对象进行处理并包装',
'* 事件触发后调用',
'* @param { object } eventData 事件触发后,抛出的数据,根据组件与事件的不同而有所变化',
'* @param { object } sourceConfig 调用该方法的组件的数据源配置',
'* @param { object } globalParams 全局参数存储对象',
'* @param object this 当前render',
'* @returns { object } 被包装后的 result 对象',
'*/',
'exports.handleEventData = ({ eventData }) => {',
' // 例:单元格点击事件,获取当前行当前单元格的值',
' const { data: { row = {}, key }} = eventData',
' let result = []',
' result.push({ label: row[key].name })',
' return { data: result }',
'}'
].join('\n')
export const DEFAULT_CODE_Style = [
'/**',
'* 稍后补充',
'*/'
].join('\n')
export const FUN_EXAMPLE = [
'exports.setCustom_1656661610040 = ({ store, event, data }, self) => {',
' // mtd 全局提示',
' self.$message.warning("请求失败");',
'',
' // mtd 确认框',
' self.$mtd.confirm({',
' title: "标题",',
' message: "内容"',
' });',
'',
' // store存储',
' self.setStore("city", "北京")',
'',
' // store获取url参数(页面初始化时url参数已经被获取存储在store.query对象中)',
' store.query.id',
'',
' // feroUi全局loading',
' feroUi.loading.show()',
' feroUi.loading.hide()',
'',
' // 时间处理dayjs',
' dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss")',
'',
' // lodash',
' _.set(a, "b", 2)',
'',
' // dataUtils时间格式化',
' self.dataUtils.dateFormat(new Date(), "yyyy-MM-dd hh:mm:ss")',
'',
' // dataUtils 获取url参数',
' self.dataUtils.getQueryString.call(self, "id")',
'',
' // dataUtils copyText',
' self.dataUtils.copyText("我是复制文字")',
'',
' // 当作为子页面时,获取父级页面渲染器实例',
' self.$parentRender.getStore("query.id")',
'}'
].join('\n')
// rule配置
export const linterConfig = {
env: {
browser: true,
node: true,
es6: true,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
plugins: ["vue"],
extends: ["eslint:recommended", "plugin:vue/essential"],
rules: {
"no-undef": "error", // 未声明变量
"no-unused-vars": "warn", // 定义了未使用
"comma-dangle": "warn", // 警告使用拖尾逗号
"no-cond-assign": "error", // 禁止条件表达式中的赋值运算符
"no-console": "warn", // 警告console
"no-constant-condition": "error", // 禁止在条件中使用常量表达式
"no-control-regex": "error", // 禁止在正则表达式中使用控制字符
"no-debugger": "warn", // 警告debugger
"no-dupe-args": "error", // 不允许 function 定义中的重复参数
"no-dupe-keys": "error", // 禁止对象字面量中的重复键
"no-duplicate-case": "error", // 不允许重复的案例标签
"no-empty": "error", // 禁止空块语句
"no-empty-character-class": "error", // 禁止在正则表达式中使用空字符类
"no-ex-assign": "error", // 不允许在 catch 子句中重新分配异常
"no-extra-boolean-cast": "error", // 禁止不必要的布尔类型转换
"no-extra-parens": "warn", // 警告不必要的括号
"no-extra-semi": "error", // 禁止不必要的分号
"no-func-assign": "error", // 不允许重新分配 function 声明
"no-inner-declarations": "error", // 禁止嵌套块中的变量或 function 声明
"no-invalid-regexp": "error", // 禁止在 RegExp 构造函数中使用无效的正则表达式字符串
"no-irregular-whitespace": "error", // 禁止不规则空格
"no-obj-calls": "error", // 禁止将全局对象属性作为函数调用
"no-regex-spaces": "error", // 禁止在正则表达式中使用多个空格
"no-sparse-arrays": "error", // 禁止稀疏数组
"no-unexpected-multiline": "error", // 禁止混淆多行表达式
"no-unreachable": "warn", // 在 return、throw、continue 和 break 语句之后警告无法访问的代码
"no-unsafe-finally": "error", //不允许 finally 块内的 return、throw、break 和 continue 语句
"use-isnan": "error", // 警告错误的 JSDoc 注释
"valid-typeof": "error", // 强制将 typeof 表达式与有效字符串进行比较
},
"globals": {
"_": true,
"dayjs": true,
"feroUi": true,
"banma": true,
}
};
webpack.config.js
javascript
var HtmlWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const path = require('path')
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
module.exports = {
mode: process.env.NODE_ENV || "development", // "production" | "development" | "none"
entry: {
demo: "../demo/index.js",
preview: "../demo/preview.js"
}, // string | object | array // 这里应用程序开始执行
output: {
publicPath: process.env.PUBLIC_URL || "",
path: path.resolve(__dirname, '../', "build"), // string
// 所有输出文件的目标路径
// 必须是绝对路径(使用 Node.js 的 path 模块)
filename: "[name].bundle.[hash:8].js" // string
},
module: {
// 关于模块配置
rules: [
// 模块规则(配置 loader、解析器等选项)
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.scss$/,
use: [
{
loader: "style-loader" // 将 JS 字符串生成为 style 节点
},
{
loader: "css-loader" // 将 CSS 转化成 CommonJS 模块
},
{
loader: "sass-loader" // 将 Sass 编译成 CSS
}
]
},
{
test: /\.(svg|eot|ttf|woff2?)$/,
loader: 'url-loader',
options: {
limit: 1
}
}
]
/* 高级模块配置(点击展示) */
},
resolve: {
// 解析模块请求的选项
// (不适用于对 loader 解析)
// 用于查找模块的目录
extensions: [".js", ".json", ".jsx", ".css", ".vue"],
// 使用的扩展名
alias: {
},
/* 可供选择的别名语法(点击展示) */
/* 高级解析选项(点击展示) */ },
performance: {
hints: "warning", // 枚举 maxAssetSize: 200000, // 整数类型(以字节为单位)
maxEntrypointSize: 400000, // 整数类型(以字节为单位)
assetFilter: function(assetFilename) {
// 提供资源文件名的断言函数
return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
}
},
devtool: "source-map", // enum // 通过在浏览器调试工具(browser devtools)中添加元信息(meta info)增强调试
// 牺牲了构建速度的 `source-map' 是最详细的。
context: __dirname, // string(绝对路径!)
// webpack 的主目录
// entry 和 module.rules.loader 选项
// 相对于此目录解析
target: "web", // 枚举 // 包(bundle)应该运行的环境
// 更改 块加载行为(chunk loading behavior) 和 可用模块(available module)
externals: { // 不要遵循/打包这些模块,而是在运行时从环境中请求他们
'vue': 'Vue'
},
stats: "errors-only", // 精确控制要显示的 bundle 信息
devServer: {
proxy: { // proxy URLs to backend development server
'/zero': {
target: 'http://zero.fe.dev.sankuai.com',
changeOrigin: true
},
'/api': {
target: 'http://yapi.bmp.sankuai.com/mock/552/api/',
changeOrigin: true
}
},
host: 'local.zero.fe.dev.sankuai.com',
port: '9998',
disableHostCheck: true,
contentBase: path.join(__dirname, 'build'), // boolean | string | array, static file location
compress: true, // enable gzip compression
historyApiFallback: true, // true for index.html upon 404, object for multiple paths
hot: true, // hot module replacement. Depends on HotModuleReplacementPlugin
https: false, // true for self-signed, object for cert authority
open: true
// ...
},
plugins: [
new VueLoaderPlugin(),
new MonacoWebpackPlugin({
languages: ['json', 'javascript', 'typescript', 'css']
}) ,
new HtmlWebpackPlugin({
title: '可视化编辑器',
template: '../demo/index.html',
chunks: ['demo']
}),
new HtmlWebpackPlugin({
title: '预览区域',
filename: 'preview.html',
template: '../demo/preview.html',
chunks: ['preview']
}),
]
}
package.json
json
{
"name": "@zero/zero-engine-vue-pricing",
"version": "1.0.69",
"description": "增加Onedata平台预览",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server --config config/webpack.config.js",
"build": "webpack --config config/webpack.config.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"@antv/event-emitter": "^0.1.2",
"@antv/g6": "^4.3.2",
"@zero/m-s-render-mtd-ui": "0.0.2",
"@zero/s-render": "0.0.2-8",
"@zero/zero-eslint": "^1.0.0",
"d3-axis": "^2.0.0",
"d3-scale": "^3.2.3",
"d3-selection": "^2.0.0",
"generate-schema": "^2.6.0",
"insert-css": "^2.0.0",
"json5": "^2.2.1",
"jsondiffpatch": "^0.4.1",
"jsplumb": "^2.15.5",
"localforage": "^1.9.0",
"mockjs": "^1.1.0",
"normalize.css": "^8.0.1",
"sortablejs": "^1.10.2",
"vue-json-editor": "^1.4.3",
"vue-json-tool": "^1.0.4",
"vue-json-viewer": "^2.2.22",
"vue-splitpane": "^1.0.6",
"vue-text-highlight": "^2.0.10"
},
"devDependencies": {
"viser-vue": "^2.4.8",
"babel-plugin-component": "^1.1.1",
"css-loader": "^5.0.0",
"file-loader": "^6.1.1",
"html-webpack-plugin": "^4.5.0",
"lodash-webpack-plugin": "^0.11.5",
"monaco-editor": "0.26.1",
"monaco-editor-webpack-plugin": "4.1.1",
"sass": "^1.27.0",
"sass-loader": "^10.0.4",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"vue-loader": "^15.9.4",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
},
"files": [
"src"
]
}