feat: 添加页面

This commit is contained in:
bqy_fe 2021-05-06 20:10:19 +08:00
parent ea986f3e6e
commit 6471c17ac7
17 changed files with 352 additions and 234 deletions

View File

@ -23,40 +23,38 @@ module.exports = {
],
rules: {
'vue/require-default-prop': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off'
// '@typescript-eslint/no-unused-vars': [
// 'error',
// {
// argsIgnorePattern: '^_',
// varsIgnorePattern: '^_'
// }
// ],
// 'no-unused-vars': [
// 'error',
// {
// argsIgnorePattern: '^_',
// varsIgnorePattern: '^_'
// }
// ],
// 'vue/html-self-closing': [
// 'error',
// {
// html: {
// void: 'always',
// normal: 'never',
// component: 'always'
// },
// svg: 'always',
// math: 'always'
// }
// ]
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'never',
component: 'always'
},
svg: 'always',
math: 'always'
}
]
},
settings: {}
}

View File

@ -1,15 +1,18 @@
<template>
<div>
<router-view #="{ Component }">
<component :is="Component" />
</router-view>
</div>
<router-view #="{ Component, route }">
<component :is="Component" :key="route.path" />
</router-view>
</template>
<script lang="ts">
export default {
name: 'App'
}
import { defineComponent } from 'vue'
export default defineComponent({
name: 'App',
setup() {
return {}
}
})
</script>
<style>

View File

@ -1,13 +1,17 @@
<template>
<template v-for="outItem in jsonData" :key="outItem._vid">
<template v-for="outItem in currentPage" :key="outItem._vid">
<slot-item :element="outItem" :config="visualConfig" />
</template>
</template>
<script lang="tsx">
import { defineComponent, PropType, reactive, toRefs } from 'vue'
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import { Toast } from 'vant'
import { visualConfig } from '@/visual.config'
import { VisualEditorModelValue } from '@/visual-editor/visual-editor.utils'
import SlotItem from './slot-item.vue'
import router from '../router'
/**
* @name: preview
* @author: 卜启缘
@ -20,18 +24,28 @@ export default defineComponent({
components: {
SlotItem
},
emits: ['update:visible'],
setup(props) {
setup() {
const jsonData: VisualEditorModelValue = JSON.parse(localStorage.getItem('jsonData') as string)
if (!jsonData || !Object.keys(jsonData.pages)) {
Toast.fail('当前没有可以预览的页面!')
}
const route = router.currentRoute
const state = reactive({
jsonData: JSON.parse(sessionStorage.getItem('blocks') || '{}')
currentPage: jsonData.pages[route.value.path]?.blocks
})
//
if (!state.currentPage) {
router.replace('/')
}
//
const renderCom = (element) => {
if (Array.isArray(element)) {
return element.map((item) => renderCom(item))
}
const component = props.config.componentMap[element.componentKey]
const component = visualConfig.componentMap[element.componentKey]
return component.render({
size: {},
@ -54,11 +68,14 @@ export default defineComponent({
<style lang="scss">
.h5-preview {
overflow: hidden;
.el-dialog__header {
display: none;
}
.simulator {
padding-right: 0;
&::-webkit-scrollbar {
width: 0;
}

View File

@ -1,44 +1,25 @@
<template>
<!-- <router-view #="{ Component }">-->
<!-- <component :is="Component" />-->
<!-- </router-view>-->
<visual-editor
v-model="jsonData"
:config="visualConfig"
:form-data="formData"
:custom-props="customProps"
/>
<visual-editor />
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import { defineComponent, provide } from 'vue'
import VisualEditor from '@/visual-editor/index.vue'
import { visualConfig } from './visual.config'
import { initVisualData, injectKey, localKey } from '@/visual-editor/hooks/useVisualData'
export default defineComponent({
name: 'App',
components: { VisualEditor },
setup() {
const state = reactive({
jsonData: {
container: {
height: 500,
width: 800
},
blocks: JSON.parse(sessionStorage.getItem('blocks') || '[]')
},
formData: [],
customProps: {}
})
const visualData = initVisualData()
//
provide(injectKey, visualData)
const { jsonData } = visualData
window.addEventListener('beforeunload', () => {
sessionStorage.setItem('blocks', JSON.stringify(state.jsonData.blocks))
sessionStorage.setItem(localKey, JSON.stringify(jsonData))
})
return {
...toRefs(state),
visualConfig
}
}
})
</script>

View File

@ -27,23 +27,21 @@
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import Preview from './preview.vue'
import { useVisualData, localKey } from '@/visual-editor/hooks/useVisualData'
export default defineComponent({
name: 'Header',
components: { Preview },
props: {
jsonData: {
type: Object,
default: () => ({})
}
},
setup(props) {
setup() {
const state = reactive({
isShowH5Preview: false
})
const { jsonData } = useVisualData()
const runPreview = () => {
sessionStorage.setItem('blocks', JSON.stringify(props.jsonData.blocks))
sessionStorage.setItem(localKey, JSON.stringify(jsonData))
localStorage.setItem(localKey, JSON.stringify(jsonData))
state.isShowH5Preview = true
}

View File

@ -2,7 +2,7 @@
<el-dialog v-model="dialogVisible" custom-class="h5-preview" :show-close="false" width="360px">
<iframe
style="width: 360px; height: 640px"
:src="`${BASE_URL}preview/#/`"
:src="previewUrl"
frameborder="0"
scrolling="auto"
></iframe>
@ -10,9 +10,8 @@
</template>
<script lang="tsx">
import { defineComponent, reactive, watch, toRefs } from 'vue'
import { defineComponent, reactive, toRefs } from 'vue'
import { useVModel } from '@vueuse/core'
import { cloneDeep } from 'lodash'
import { BASE_URL } from '@/visual-editor/utils'
/**
* @name: preview
@ -33,37 +32,11 @@ export default defineComponent({
setup(props, { emit }) {
const state = reactive({
dialogVisible: useVModel(props, 'visible', emit),
jsonDataClone: cloneDeep(props.jsonData)
previewUrl: `${BASE_URL}preview/${location.hash}`
})
watch(
() => state.dialogVisible,
(val) => {
if (val) {
state.jsonDataClone = cloneDeep(props.jsonData)
}
}
)
const renderCom = (element) => {
if (Array.isArray(element)) {
return element.map((item) => renderCom(item))
}
const component = props.config.componentMap[element.componentKey]
return component.render({
size: {},
props: element.props || {},
block: element,
model: {},
custom: {}
})
}
return {
...toRefs(state),
BASE_URL,
renderCom
...toRefs(state)
}
}
})

View File

@ -1,9 +1,24 @@
<!--页面树-->
<template>
<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick">
<el-button type="primary" size="small" style="margin: 10px 0" icon="el-icon-plus" @click="addPage"
>添加页面</el-button
>
<el-tree
:data="pages"
:props="defaultProps"
node-key="path"
highlight-current
:current-node-key="currentNodeKey"
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>{{ node.label }}</span>
<span
>{{ node.label }}{{ data.path }}
<template v-if="data.isDefault">
<el-tag size="mini">默认</el-tag>
</template>
</span>
<span @click.stop>
<el-dropdown trigger="click">
<span class="el-dropdown-link">
@ -11,12 +26,15 @@
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="el-icon-plus" @click="addPage(data)"
>添加子页面</el-dropdown-item
<el-dropdown-item icon="el-icon-edit" @click="editPage(data)"
>编辑</el-dropdown-item
>
<el-dropdown-item icon="el-icon-delete" @click="delPage(data)"
>删除</el-dropdown-item
>
<el-dropdown-item icon="el-icon-link" @click="setDefaultPage(data)"
>设为默认</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
@ -24,39 +42,119 @@
</span>
</template>
</el-tree>
<el-dialog
v-model="dialogFormVisible"
width="380px"
:title="operatePageData ? '编辑页面' : '新增页面'"
>
<el-form :model="form">
<el-form-item label="页面标题" label-width="80px">
<el-input v-model="form.title" autocomplete="off" />
</el-form-item>
<el-form-item label="页面路径" label-width="80px">
<el-input v-model="form.path" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false"> </el-button>
<el-button type="primary" @click="onSubmit"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import { treeData } from './treeData'
import { defineComponent, reactive, computed, toRefs } from 'vue'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
import { useRouter } from 'vue-router'
export default defineComponent({
name: 'PageTree',
setup() {
const router = useRouter()
const { jsonData, setCurrentPage, deletePage, updatePage, incrementPage } = useVisualData()
const state = reactive({
data: treeData,
defaultProps: {
children: 'children',
label: 'label'
label: 'title'
},
currentNodeKey: location.hash.slice(1),
dialogFormVisible: false, //
operatePageData: null as any, //
form: {
//
title: '',
path: ''
}
})
//
const pages = computed(() =>
Object.keys(jsonData.pages).map((key) => ({
title: jsonData.pages[key].title,
path: key
}))
)
//
const handleNodeClick = (data) => {
console.log(data)
setCurrentPage(data.path)
router.push(data.path)
}
//
const addPage = (data) => {
//
const addPage = () => {
state.operatePageData = null
state.form = {
title: '',
path: ''
}
state.dialogFormVisible = true
}
//
const editPage = (data) => {
state.operatePageData = data
state.form = {
title: data.title,
path: data.path
}
state.dialogFormVisible = true
console.log('子页面数据:', data)
}
//
const delPage = (data) => {
console.log('删除子页面数据', data)
deletePage(data.path, '/')
}
//
const setDefaultPage = (data) => {
console.log('设置该页面为默认页面', data)
}
//
const onSubmit = async () => {
const { title, path } = state.form
if (state.operatePageData) {
updatePage({ newPath: path, oldPath: state.operatePageData.path || path, page: { title } })
await router.replace(path)
state.currentNodeKey = path
} else {
incrementPage(path, { title, blocks: [] })
}
state.dialogFormVisible = false
}
return {
...toRefs(state),
setCurrentPage,
pages,
onSubmit,
setDefaultPage,
handleNodeClick,
addPage,
editPage,
delPage
}
}

View File

@ -1,57 +1,7 @@
export const treeData = [
{
label: '一级 1',
children: [
{
label: '二级 1-1',
children: [
{
label: '三级 1-1-1'
}
]
}
]
},
{
label: '一级 2',
children: [
{
label: '二级 2-1',
children: [
{
label: '三级 2-1-1'
}
]
},
{
label: '二级 2-2',
children: [
{
label: '三级 2-2-1'
}
]
}
]
},
{
label: '一级 3',
children: [
{
label: '二级 3-1',
children: [
{
label: '三级 3-1-1'
}
]
},
{
label: '二级 3-2',
children: [
{
label: '三级 3-2-1'
}
]
}
]
title: '首页',
path: '/',
isDefault: true
}
]

View File

@ -9,7 +9,7 @@
{{ tabItem.label }}
</div>
</template>
<component :is="tabItem.componentName" />
<component :is="tabItem.componentName" v-bind="$attrs" />
</el-tab-pane>
</template>
</el-tabs>

View File

@ -1,6 +1,6 @@
import { defineComponent, PropType } from 'vue'
import { VisualEditorProps } from '../../../../visual-editor.props'
import { useModel } from '../../../../utils/useModel'
import { useModel } from '../../../../hooks/useModel'
import { ElButton, ElTag } from 'element-plus'
import { $$tablePropEditor } from './table-prop-edit.service'

View File

@ -23,23 +23,20 @@ import {
} from 'element-plus'
import { VisualEditorProps, VisualEditorPropsType } from '@/visual-editor/visual-editor.props'
import { TablePropEditor } from '@/visual-editor/components/right-attribute-panel/components/table-prop-editor/table-prop-editor'
import {
VisualEditorBlockData,
VisualEditorConfig,
VisualEditorModelValue
} from '@/visual-editor/visual-editor.utils'
import { VisualEditorBlockData } from '@/visual-editor/visual-editor.utils'
import MonacoEditor from './MonacoEditor'
import { useVModel } from '@vueuse/core'
import { useDotProp } from '@/visual-editor/utils/useDotProp'
import { useDotProp } from '@/visual-editor/hooks/useDotProp'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
export default defineComponent({
name: 'RightAttributePanel',
props: {
block: { type: Object as PropType<VisualEditorBlockData>, default: () => ({}) },
config: { type: Object as PropType<VisualEditorConfig>, required: true },
dataModel: { type: Object as PropType<{ value: VisualEditorModelValue }>, required: true }
block: { type: Object as PropType<VisualEditorBlockData>, default: () => ({}) }
},
setup(props, { emit }) {
const { visualConfig } = useVisualData()
const state = reactive({
activeName: 'attr',
isOpen: true,
@ -85,7 +82,7 @@ export default defineComponent({
)
} else {
const { componentKey } = props.block
const component = props.config.componentMap[componentKey]
const component = visualConfig.componentMap[componentKey]
console.log('props.block:', props.block)
content.push(
<ElFormItem label="组件ID" labelWidth={'76px'}>

View File

@ -1,5 +1,5 @@
<template>
<draggable-transition-group v-model:drag="drag" v-model="VMBlocks">
<draggable-transition-group v-model:drag="drag" v-model="currentPage.blocks">
<template #item="{ element: outElement }">
<div
class="list-group-item"
@ -14,7 +14,7 @@
@mousedown="selectComp(outElement)"
>
<comp-render
:config="config"
:config="visualConfig"
:element="outElement"
:style="{
pointerEvents: Object.keys(outElement.props?.slots || {}).length ? 'auto' : 'none'
@ -24,7 +24,7 @@
<slot-item
v-model:children="value.children"
:slot-key="slotKey"
:config="config"
:config="visualConfig"
:on-contextmenu-block="onContextmenuBlock"
:select-comp="selectComp"
/>
@ -36,15 +36,15 @@
</template>
<script lang="tsx">
import { defineComponent, reactive, toRefs, Ref, PropType, SetupContext } from 'vue'
import { VisualEditorConfig, VisualEditorBlockData } from '@/visual-editor/visual-editor.utils'
import { useVModel } from '@vueuse/core'
import { defineComponent, reactive, toRefs, SetupContext } from 'vue'
import { VisualEditorBlockData } from '@/visual-editor/visual-editor.utils'
import DraggableTransitionGroup from './draggable-transition-group.vue'
import { $$dropdown, DropdownOption } from '@/visual-editor/utils/dropdown-service'
import CompRender from './comp-render'
import SlotItem from './slot-item.vue'
import { cloneDeep } from 'lodash'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
export default defineComponent({
name: 'SimulatorEditor',
@ -53,18 +53,15 @@ export default defineComponent({
CompRender,
SlotItem
},
props: {
blocks: { type: Array as PropType<VisualEditorBlockData[]>, required: true },
config: { type: Object as PropType<VisualEditorConfig>, required: true }
},
emits: ['update:blocks', 'on-selected'],
setup(props, { emit }: SetupContext) {
emits: ['on-selected'],
setup(_, { emit }: SetupContext) {
const { globalProperties } = useGlobalProperties()
const { currentPage, visualConfig } = useVisualData()
const state = reactive({
compRefs: [],
drag: false,
VMBlocks: useVModel(props, 'blocks', emit) as Ref<VisualEditorBlockData[]>
drag: false
})
//
@ -104,7 +101,7 @@ export default defineComponent({
item.focusWithChild = false
item.focus = item._vid == _vid
if (item.focus) {
const arr = findPathByLeafId(_vid, state.VMBlocks)
const arr = findPathByLeafId(_vid, currentPage.value.blocks)
arr.forEach((n) => (n.focusWithChild = true))
}
if (Object.keys(item.props?.slots || {}).length) {
@ -117,7 +114,7 @@ export default defineComponent({
const selectComp = (element) => {
emit('on-selected', element)
state.VMBlocks.forEach((block) => {
currentPage.value.blocks.forEach((block) => {
block.focus = element._vid == block._vid
block.focusWithChild = false
handleSlotsFocus(block, element._vid)
@ -128,7 +125,7 @@ export default defineComponent({
const onContextmenuBlock = (
e: MouseEvent,
block: VisualEditorBlockData,
parentBlocks = state.VMBlocks
parentBlocks = currentPage.value.blocks
) => {
$$dropdown({
reference: e,
@ -180,6 +177,8 @@ export default defineComponent({
return {
...toRefs(state),
currentPage,
visualConfig,
selectComp,
onContextmenuBlock
}

View File

@ -0,0 +1,115 @@
/**
* @name: useVisualData
* @author:
* @date: 2021/5/6 11:59
* @descriptionuseVisualData
* @update: 2021/5/6 11:59
*/
import { reactive, inject, readonly, computed, ComputedRef, DeepReadonly } from 'vue'
import {
VisualEditorModelValue,
VisualEditorBlockData,
VisualEditorPage,
VisualEditorConfig
} from '@/visual-editor/visual-editor.utils'
import { visualConfig } from '@/visual.config'
// 保存到本地JSON数据的key
export const localKey = 'jsonData'
// 注入jsonData的key
export const injectKey = Symbol('injectKey')
interface IState {
currentPage: VisualEditorPage // 当前正在操作的页面
jsonData: VisualEditorModelValue // 整颗JSON树
}
export interface VisualData {
jsonData: DeepReadonly<VisualEditorModelValue> // 保护JSONData避免直接修改
currentPage: ComputedRef<VisualEditorPage> // 当前正在操作的页面
visualConfig: VisualEditorConfig // 组件配置
updatePage: (data: { newPath?: string; oldPath: string; page: Partial<VisualEditorPage> }) => void // 更新某个页面
incrementPage: (path: string, page: VisualEditorPage) => void // 新增页面
deletePage: (path: string, redirect?: string) => void // 删除页面
updatePageBlock: (path: string, blocks: VisualEditorBlockData[]) => void // 更新某页面下的所有组件
setCurrentPage: (path: string) => void // 设置当前正在操作的页面
}
export const initVisualData = (): VisualData => {
const jsonData: VisualEditorModelValue = JSON.parse(
sessionStorage.getItem('jsonData') as string
) || {
container: {
width: 360,
height: 960
},
pages: {
'/': {
title: '首页',
blocks: []
}
}
}
console.log('jsonData', jsonData)
// 获取当前页面哈希路径
const getCurrentPath = () => location.hash.slice(1)
// 所有页面的path都必须以 / 开发
const getPrefixPath = (path: string) => (path.startsWith('/') ? path : `/${path}`)
const state: IState = reactive({
jsonData,
currentPage: jsonData.pages[getCurrentPath()] ?? jsonData.pages['/']
})
// 路由变化时更新当前操作的页面
window.addEventListener('hashchange', () => {
setCurrentPage(getCurrentPath())
})
// 更新page
const updatePage = ({ newPath, oldPath, page }) => {
if (newPath && newPath != oldPath) {
// 如果传了新的路径,则认为是修改页面路由
state.jsonData.pages[getPrefixPath(newPath)] = { ...state.jsonData.pages[oldPath], ...page }
deletePage(oldPath, getPrefixPath(newPath))
} else {
Object.assign(state.jsonData.pages[oldPath], page)
}
}
// 添加page
const incrementPage = (path = '', page: VisualEditorPage = { title: '新页面', blocks: [] }) => {
state.jsonData.pages[getPrefixPath(path)] = page
}
// 删除page
const deletePage = (path = '', redirectPath = '') => {
delete state.jsonData.pages[path]
if (redirectPath) {
setCurrentPage(redirectPath)
}
}
// 设置当前页面
const setCurrentPage = (path = '/') => {
state.currentPage = jsonData.pages[path]
}
// 更新pages下面的blocks
const updatePageBlock = (path = '', blocks: VisualEditorBlockData[] = []) => {
state.jsonData.pages[path].blocks = blocks
}
return {
jsonData: readonly(state.jsonData), // 保护JSONData避免直接修改
currentPage: computed(() => state.currentPage),
visualConfig,
setCurrentPage,
updatePage,
incrementPage,
deletePage,
updatePageBlock
}
}
export const useVisualData = () => inject<VisualData>(injectKey)!

View File

@ -2,7 +2,7 @@
<el-container>
<el-header height="80px" class="flex items-center shadow-md">
<!-- 顶部start -->
<Header :json-data="modelValue" :config="config" />
<Header />
<!-- 顶部end -->
</el-header>
<el-container class="layout-container">
@ -14,20 +14,12 @@
<el-main>
<!-- 中间编辑区域start -->
<simulator>
<simulator-editor
v-model:blocks="dataModel.blocks"
:config="config"
@on-selected="onSelected"
/>
<simulator-editor @on-selected="onSelected" />
</simulator>
<!-- 中间编辑区域end -->
<!-- 右侧属性面板start -->
<right-attribute-panel
v-model:block="currentBlock"
:config="config"
:data-model="dataModel"
/>
<right-attribute-panel v-model:block="currentBlock" />
<!-- 右侧属性面板end -->
</el-main>
</el-container>
@ -35,37 +27,23 @@
</template>
<script lang="ts">
import { defineComponent, PropType, reactive, toRefs } from 'vue'
import { defineComponent, reactive, toRefs } from 'vue'
import Header from './components/header/index.vue'
import LeftAside from './components/left-aside/index.vue'
import RightAttributePanel from './components/right-attribute-panel'
import SimulatorEditor from './components/simulator-editor/simulator-editor.vue'
import Simulator from './components/common/simulator.vue'
import {
VisualEditorConfig,
VisualEditorModelValue,
VisualEditorComponent
} from '@/visual-editor/visual-editor.utils'
import { useVModel } from '@vueuse/core'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
interface IState {
dataModel: VisualEditorModelValue
currentBlock: VisualEditorComponent
}
export default defineComponent({
name: 'Layout',
components: { Header, LeftAside, RightAttributePanel, SimulatorEditor, Simulator },
props: {
modelValue: { type: Object as PropType<VisualEditorModelValue>, required: true },
config: { type: Object as PropType<VisualEditorConfig>, required: true },
formData: { type: Object as PropType<Record<string, any>>, required: true },
customProps: { type: Object as PropType<Record<string, any>> }
},
emits: ['update:modelValue'],
setup(props) {
setup() {
const state: IState = reactive({
dataModel: useVModel(props, 'modelValue'),
currentBlock: {} as VisualEditorComponent
})

View File

@ -1,6 +1,6 @@
import { VisualEditorProps } from './visual-editor.props'
import { inject, provide } from 'vue'
import { useDotProp } from '@/visual-editor/utils/useDotProp'
import { useDotProp } from '@/visual-editor/hooks/useDotProp'
export interface VisualEditorBlockData {
_vid: string // 组件id 时间戳
@ -21,12 +21,23 @@ export interface VisualEditorBlockData {
[prop: string]: any
}
export interface VisualEditorPage {
title: string // 页面标题
isDefault?: boolean // 404是重定向到默认页面
blocks: VisualEditorBlockData[] // 当前页面的所有组件
}
export interface VisualEditorPages {
[path: string]: VisualEditorPage
}
export interface VisualEditorModelValue {
container: {
width: number
height: number
}
blocks?: VisualEditorBlockData[]
// 页面
pages: VisualEditorPages
}
export interface VisualEditorComponent {