mirror of
https://github.com/sahadev/vue-component-creater-ui.git
synced 2025-06-07 21:54:05 +08:00
535 lines
13 KiB
Vue
535 lines
13 KiB
Vue
<template>
|
||
<div style="diplay:flex;height:100%;">
|
||
<div class="main-main">
|
||
<nav class="base-component-container">
|
||
<raw-components></raw-components>
|
||
</nav>
|
||
|
||
<div class="main-container">
|
||
<!--顶部工具栏-->
|
||
<tools-bar @onPreviewModeChange="onPreviewModeChange" @onEditModeChange="onEditModeChange" @redo="redo"
|
||
@undo="undo" @structureVisible="structureVisible = true"></tools-bar>
|
||
|
||
<div class="preview-container">
|
||
<div id="render-control-panel">
|
||
<!--这里不能放任何东西,执行时会被清空-->
|
||
</div>
|
||
</div>
|
||
<attribute-input :enableRemoveButton="true" class="attribute" @save="onSaveAttr" @remove="onRemove"
|
||
ref="attributeInput" shortcutInitMode="hand" :__rawVueInfo__="currentEditRawInfo">
|
||
</attribute-input>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="copy">
|
||
<div>
|
||
<el-alert title="遇到问题?" type="info">
|
||
<el-link :underline="false" @click="help" style="font-size: 12px; margin-top: 5px;">点击我查看帮助文档</el-link>
|
||
</el-alert>
|
||
</div>
|
||
|
||
<el-tooltip effect="dark" content="二次编辑" placement="top-start">
|
||
<div class="round-icon icon-vue" alt="" @click="vueDialogVisible = true">Vue</div>
|
||
</el-tooltip>
|
||
<el-tooltip effect="dark" content="编辑JS逻辑" placement="top-start">
|
||
<div class="round-icon icon-js" alt="" @click="jsDialogVisible = true">JS</div>
|
||
</el-tooltip>
|
||
<el-tooltip effect="dark" content="查看实时代码" placement="top-start">
|
||
<img class="round-icon" :src="iconCode" alt="" @click="codeDialogVisible = true">
|
||
</el-tooltip>
|
||
<el-popconfirm confirmButtonText="确认" cancelButtonText="点错了" icon="el-icon-info" iconColor="red"
|
||
title="点我将清空所有编辑的内容, 确认吗?" @confirm="clear">
|
||
<template #reference>
|
||
<img class="round-icon" :src="iconClear" alt="">
|
||
</template>
|
||
</el-popconfirm>
|
||
</div>
|
||
|
||
<div>
|
||
<lc-code :rawCode="code" v-model:codeDialogVisible="codeDialogVisible">
|
||
</lc-code>
|
||
<code-structure @save="onSaveAttr" @remove="onRemove" ref="codeStructure" v-model="structureVisible"
|
||
@reRender="render">
|
||
</code-structure>
|
||
<CodeEditor v-model:codeDialogVisible="jsDialogVisible" @saveJSCode="saveJSCode" ref="codeEditor"></CodeEditor>
|
||
<VueEditor v-model:vueDialogVisible="vueDialogVisible" @codeParseSucess="codeParseSucess"></VueEditor>
|
||
</div>
|
||
|
||
<!-- 辅助定位线 -->
|
||
<div class="cross-flag">
|
||
<div class="x"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="fullScreen" v-if="!editMode">
|
||
<div style="margin: 20px; font-weight: bold;">按下ESC退出预览模式</div>
|
||
<div id="mountedEle"></div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { defineAsyncComponent } from 'vue'
|
||
import { splitInit } from "../libs/split-init";
|
||
// // 这个文件不可以进行懒加载,它会导致运行时不可点击的行为,具体原因未知
|
||
import { MainPanelProvider } from "../libs/main-panel";
|
||
import { initContainerForLine } from "@/utils/lineHelper";
|
||
import keymaster from "keymaster"
|
||
|
||
export default {
|
||
name: "vcc",
|
||
props: {
|
||
initCodeEntity: {
|
||
type: Object,
|
||
default: () => {
|
||
return {};
|
||
}
|
||
}
|
||
},
|
||
emits: ['updateCodeEntity'],
|
||
components: {
|
||
RawComponents: defineAsyncComponent(() => import("@/components/RawComponents.vue")),
|
||
ToolsBar: defineAsyncComponent(() => import("./ToolsBar")),
|
||
AttributeInput: defineAsyncComponent(() => import("../components/AttributeInput")),
|
||
CodeStructure: defineAsyncComponent(() => import("../components/CodeStructure")),
|
||
"lc-code": defineAsyncComponent(() => import("../components/Code")),
|
||
CodeEditor: defineAsyncComponent(() => import('../components/JSCodeEditorDialog.vue')),
|
||
VueEditor: defineAsyncComponent(() => import('../components/VueCodeParseDialog.vue'))
|
||
},
|
||
data() {
|
||
return {
|
||
currentEditRawInfo: null,
|
||
code: "",
|
||
codeDialogVisible: false,
|
||
structureVisible: false,
|
||
jsDialogVisible: false,
|
||
vueDialogVisible: false,
|
||
iconCode: ("https://static.imonkey.xueersi.com/download/vcc-resource/icon/code-working-outline.svg"),
|
||
iconClear: ("https://static.imonkey.xueersi.com/download/vcc-resource/icon/trash-outline.svg"),
|
||
|
||
editMode: true,
|
||
|
||
codeRawVueInfo: "",
|
||
JSCode: ""
|
||
};
|
||
},
|
||
watch: {
|
||
currentEditRawInfo(newValue) {
|
||
const attributeContainter = document.querySelector(".attribute");
|
||
if (newValue) {
|
||
attributeContainter.style = "right:10px; display:block;";
|
||
this.$refs['attributeInput'].onShow();
|
||
} else {
|
||
attributeContainter.style = "right: calc(-300px - 20px); display:none;";
|
||
this.$refs['attributeInput'].onHide();
|
||
}
|
||
},
|
||
initCodeEntity(newVal) {
|
||
if (newVal.JSCode) {
|
||
this.mainPanelProvider.saveJSCodeOnly(this.convertLogicCode(newVal.JSCode));
|
||
}
|
||
|
||
if (newVal.codeStructure) {
|
||
this.mainPanelProvider.render(newVal.codeStructure);
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
},
|
||
beforeCreate() { },
|
||
created() {
|
||
this.mainPanelProvider = new MainPanelProvider();
|
||
},
|
||
beforeMount() { },
|
||
mounted() {
|
||
Promise.all([import("../map/load")])
|
||
.then(res => {
|
||
this.$emit("onLoadFinish");
|
||
this.init();
|
||
});
|
||
splitInit();
|
||
this.initShortcut();
|
||
},
|
||
beforeUpdate() { },
|
||
updated() { },
|
||
destoryed() { },
|
||
methods: {
|
||
convertLogicCode(JSCode) {
|
||
try {
|
||
const JSCodeInfo = eval(`(function(){return ${JSCode.replace(/\s+/g, "")}})()`);
|
||
// 保留JS代码
|
||
this.JSCode = JSCode;
|
||
if (this.$refs.codeEditor) {
|
||
this.$refs.codeEditor.updateLogicCode(JSCode);
|
||
}
|
||
return JSCodeInfo;
|
||
} catch (e) {
|
||
console.warn(`外部逻辑代码解析出错,解析的逻辑代码为: ${JSCode}, Error: ${e}`);
|
||
}
|
||
},
|
||
|
||
initShortcut() {
|
||
keymaster('⌘+z, ctrl+z', () => {
|
||
this.undo();
|
||
return false
|
||
});
|
||
|
||
|
||
keymaster('esc', () => {
|
||
this.editMode = true;
|
||
this.mainPanelProvider.setEditMode(true);
|
||
return false
|
||
});
|
||
},
|
||
|
||
init() {
|
||
// 先订阅事件再渲染
|
||
this.mainPanelProvider.onRootElementMounted(rootElement => {
|
||
document.getElementsByTagName('body')[0].addEventListener("click", () => {
|
||
this.mainPanelProvider.clearElementSelect();
|
||
})
|
||
|
||
// 只针对根div做事件监听
|
||
initContainerForLine(rootElement.firstChild, this.currentPointer);
|
||
|
||
document.querySelector(".x").style = "display:none;";
|
||
|
||
}).onMerged(() => {
|
||
this.currentPointer(null);
|
||
}).onCodeCreated(code => {
|
||
this.code = code;
|
||
}).onCodeStructureUpdated(codeRawVueInfo => {
|
||
if (this.$refs.codeStructure) {
|
||
this.$refs.codeStructure.updateCode(codeRawVueInfo);
|
||
}
|
||
this.codeRawVueInfo = codeRawVueInfo;
|
||
|
||
this.notifyParent();
|
||
}).onNodeDeleted(() => {
|
||
this.currentEditRawInfo = null;
|
||
}).onSelectElement(rawInfo => {
|
||
this.currentEditRawInfo = rawInfo;
|
||
}).saveJSCodeOnly(this.convertLogicCode(this.initCodeEntity.JSCode ? this.initCodeEntity.JSCode : ''))
|
||
.render(this.initCodeEntity.codeStructure ? this.initCodeEntity.codeStructure : this.getFakeData());
|
||
},
|
||
|
||
// 通知父组件
|
||
notifyParent() {
|
||
this.$emit('updateCodeEntity', {
|
||
codeRawVueInfo: this.codeRawVueInfo,
|
||
JSCode: this.JSCode
|
||
});
|
||
},
|
||
|
||
// 指向将要插入哪个元素之前
|
||
currentPointer(ele, index) {
|
||
this.mainPanelProvider.setDropInfo({
|
||
target: ele,
|
||
index,
|
||
});
|
||
},
|
||
|
||
/**获取一个模拟的实体对象 */
|
||
getFakeData() {
|
||
return {
|
||
template: {
|
||
lc_id: "root",
|
||
__children: [{
|
||
div: {
|
||
class: "container",
|
||
"lc_id": "container",
|
||
"style": "min-height: 100%; padding-bottom: 100px;",
|
||
__text__: "Hello,欢迎使用VCC编辑器,请往此区域拖拽组件",
|
||
}
|
||
}]
|
||
},
|
||
}
|
||
|
||
},
|
||
|
||
onPreviewModeChange(newValue) {
|
||
const previewElem = document.querySelector("#render-control-panel");
|
||
if (newValue) {
|
||
previewElem.style = "width:375px;";
|
||
} else {
|
||
previewElem.style = "width:100%;";
|
||
}
|
||
},
|
||
|
||
onEditModeChange(newValue) {
|
||
this.editMode = newValue;
|
||
|
||
this.$nextTick(() => {
|
||
this.mainPanelProvider.setEditMode(newValue, document.querySelector("#mountedEle"));
|
||
})
|
||
},
|
||
|
||
renderCode() {
|
||
this.mainPanelProvider.reRender();
|
||
},
|
||
|
||
clear() {
|
||
this.mainPanelProvider.render(this.getFakeData());
|
||
},
|
||
|
||
onSaveAttr({ resultList, lc_id }) {
|
||
this.mainPanelProvider.saveAttribute(resultList, lc_id);
|
||
},
|
||
|
||
onRemove({ lc_id }) {
|
||
this.mainPanelProvider.remove(lc_id);
|
||
},
|
||
|
||
redo() {
|
||
this.mainPanelProvider.redo();
|
||
},
|
||
undo() {
|
||
this.mainPanelProvider.undo();
|
||
},
|
||
|
||
saveJSCode({ JSCodeInfo: code, JSCode }) {
|
||
this.mainPanelProvider.saveJSCode(code);
|
||
// 保留JS代码
|
||
this.JSCode = JSCode;
|
||
this.notifyParent();
|
||
},
|
||
|
||
/**
|
||
* 二级编辑解析
|
||
*/
|
||
codeParseSucess(vueCodeEntity) {
|
||
this.mainPanelProvider.render(vueCodeEntity);
|
||
},
|
||
|
||
/**
|
||
* 渲染指定结构
|
||
*/
|
||
render(codeEntity) {
|
||
this.mainPanelProvider.render(codeEntity);
|
||
},
|
||
|
||
help() {
|
||
window.open('/doc')
|
||
}
|
||
},
|
||
fillter: {},
|
||
};
|
||
</script>
|
||
|
||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||
<style scoped lang="scss">
|
||
/* =============== 以下结果追加于: 2020/3/23 上午10:03:02 =============== */
|
||
.main-main {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
background-color: #f0f0f0;
|
||
}
|
||
|
||
.base-component-container {
|
||
border-radius: 0px;
|
||
background-color: white;
|
||
}
|
||
|
||
.main-container {
|
||
margin: 0px 0px 0 0;
|
||
display: flex;
|
||
max-height: 100vh;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.attribute {
|
||
width: 300px;
|
||
border-radius: 10px;
|
||
margin-left: 10px;
|
||
position: absolute;
|
||
right: calc(-300px - 20px);
|
||
top: 10px;
|
||
background: white;
|
||
max-height: calc(80% - 20px);
|
||
transition: right 0.5s;
|
||
overflow: scroll;
|
||
z-index: 2;
|
||
}
|
||
|
||
#render-control-panel {
|
||
height: 100%;
|
||
width: 100%;
|
||
border-radius: 0px;
|
||
overflow: scroll;
|
||
box-sizing: border-box;
|
||
background-color: white;
|
||
transition: width 1s;
|
||
padding: 10px;
|
||
}
|
||
|
||
.preview-container {
|
||
height: 0;
|
||
flex-grow: 1;
|
||
display: flex;
|
||
justify-content: center;
|
||
background-color: #f0f0f0;
|
||
}
|
||
|
||
.copy {
|
||
position: fixed;
|
||
right: 20px;
|
||
bottom: 20px;
|
||
display: flex;
|
||
line-height: 0;
|
||
}
|
||
|
||
.round-icon {
|
||
background: #4dba87;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 20px;
|
||
padding: 10px 0;
|
||
margin-left: 10px;
|
||
border: 0px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.icon-js {
|
||
line-height: 20px;
|
||
color: white;
|
||
text-align: center;
|
||
}
|
||
|
||
.icon-vue {
|
||
line-height: 20px;
|
||
color: white;
|
||
text-align: center;
|
||
}
|
||
|
||
.cross-flag {
|
||
position: fixed;
|
||
right: 0;
|
||
top: 0;
|
||
.x {
|
||
width: 20px;
|
||
height: 2px;
|
||
position: fixed;
|
||
background-color: #4dba87;
|
||
border-radius: 1px;
|
||
top: 0;
|
||
display: none;
|
||
right: 0;
|
||
pointer-events: none;
|
||
}
|
||
.y {
|
||
width: 2px;
|
||
height: 20px;
|
||
position: fixed;
|
||
background-color: #4dba87;
|
||
top: 0;
|
||
border-radius: 1px;
|
||
display: none;
|
||
right: 0;
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
#fullScreen {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: fixed;
|
||
z-index: 3;
|
||
top: 0;
|
||
background: white;
|
||
overflow: scroll;
|
||
}
|
||
|
||
#mountedEle {
|
||
border: 1px dashed rgb(126, 126, 128);
|
||
border-radius: 10px;
|
||
margin: 20px;
|
||
}
|
||
</style>
|
||
|
||
<!-- 以下的样式作用于渲染容器中-->
|
||
<style lang="scss">
|
||
#render-control-panel {
|
||
position: relative;
|
||
|
||
[div-lc-mark] {
|
||
border: 1px grey dashed;
|
||
min-height: 1rem;
|
||
border-radius: 5px;
|
||
}
|
||
|
||
[lc_id] {
|
||
}
|
||
|
||
&::after {
|
||
content: "编辑区域";
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
font-weight: 600;
|
||
font-size: 40px;
|
||
color: #d6d6d6;
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
.mark-element-unit {
|
||
opacity: 0.5;
|
||
outline: red 2px solid;
|
||
}
|
||
</style>
|
||
|
||
<style lang="scss">
|
||
.icon-s {
|
||
font-size: 14px;
|
||
color: #000;
|
||
margin: 0 2px;
|
||
}
|
||
|
||
:root {
|
||
--animate-duration: 1.5s;
|
||
}
|
||
|
||
.in-element {
|
||
outline: 2px solid #4dba87 !important;
|
||
position: relative;
|
||
}
|
||
|
||
.mark-element {
|
||
outline: 2px solid #4dba87 !important;
|
||
position: relative;
|
||
}
|
||
|
||
.mark-element::before {
|
||
content: attr(lc-component-name) !important;
|
||
background: #4dba87;
|
||
color: white;
|
||
left: 0 !important;
|
||
top: 0 !important;
|
||
transform: translateY(-100%);
|
||
position: absolute;
|
||
font-size: 12px;
|
||
line-height: 12px;
|
||
padding: 1px 2px;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.light-mark {
|
||
outline: 2px solid #4dba87;
|
||
position: relative;
|
||
}
|
||
|
||
.light-mark::before {
|
||
content: attr(lc-component-name) !important;
|
||
background: #4dba87;
|
||
color: white;
|
||
left: 0 !important;
|
||
top: 0 !important;
|
||
position: absolute;
|
||
font-size: 12px;
|
||
line-height: 12px;
|
||
padding: 1px 2px;
|
||
border-radius: 2px;
|
||
}
|
||
</style>
|