feat: 添加页面
This commit is contained in:
parent
ea986f3e6e
commit
6471c17ac7
56
.eslintrc.js
56
.eslintrc.js
|
@ -23,40 +23,38 @@ module.exports = {
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
'vue/require-default-prop': 'off',
|
'vue/require-default-prop': 'off',
|
||||||
'no-unused-vars': 'off',
|
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
|
||||||
|
|
||||||
'@typescript-eslint/ban-ts-comment': 'off',
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'@typescript-eslint/ban-types': 'off',
|
'@typescript-eslint/ban-types': 'off',
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off'
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
// '@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
// 'error',
|
'error',
|
||||||
// {
|
{
|
||||||
// argsIgnorePattern: '^_',
|
argsIgnorePattern: '^_',
|
||||||
// varsIgnorePattern: '^_'
|
varsIgnorePattern: '^_'
|
||||||
// }
|
}
|
||||||
// ],
|
],
|
||||||
// 'no-unused-vars': [
|
'no-unused-vars': [
|
||||||
// 'error',
|
'error',
|
||||||
// {
|
{
|
||||||
// argsIgnorePattern: '^_',
|
argsIgnorePattern: '^_',
|
||||||
// varsIgnorePattern: '^_'
|
varsIgnorePattern: '^_'
|
||||||
// }
|
}
|
||||||
// ],
|
],
|
||||||
// 'vue/html-self-closing': [
|
'vue/html-self-closing': [
|
||||||
// 'error',
|
'error',
|
||||||
// {
|
{
|
||||||
// html: {
|
html: {
|
||||||
// void: 'always',
|
void: 'always',
|
||||||
// normal: 'never',
|
normal: 'never',
|
||||||
// component: 'always'
|
component: 'always'
|
||||||
// },
|
},
|
||||||
// svg: 'always',
|
svg: 'always',
|
||||||
// math: 'always'
|
math: 'always'
|
||||||
// }
|
}
|
||||||
// ]
|
]
|
||||||
},
|
},
|
||||||
settings: {}
|
settings: {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<router-view #="{ Component, route }">
|
||||||
<router-view #="{ Component }">
|
<component :is="Component" :key="route.path" />
|
||||||
<component :is="Component" />
|
</router-view>
|
||||||
</router-view>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default {
|
import { defineComponent } from 'vue'
|
||||||
name: 'App'
|
|
||||||
}
|
export default defineComponent({
|
||||||
|
name: 'App',
|
||||||
|
setup() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
<template>
|
<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" />
|
<slot-item :element="outItem" :config="visualConfig" />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="tsx">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType, reactive, toRefs } from 'vue'
|
import { defineComponent, reactive, toRefs } from 'vue'
|
||||||
|
import { Toast } from 'vant'
|
||||||
import { visualConfig } from '@/visual.config'
|
import { visualConfig } from '@/visual.config'
|
||||||
|
import { VisualEditorModelValue } from '@/visual-editor/visual-editor.utils'
|
||||||
import SlotItem from './slot-item.vue'
|
import SlotItem from './slot-item.vue'
|
||||||
|
import router from '../router'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name: preview
|
* @name: preview
|
||||||
* @author: 卜启缘
|
* @author: 卜启缘
|
||||||
|
@ -20,18 +24,28 @@ export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
SlotItem
|
SlotItem
|
||||||
},
|
},
|
||||||
emits: ['update:visible'],
|
setup() {
|
||||||
setup(props) {
|
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({
|
const state = reactive({
|
||||||
jsonData: JSON.parse(sessionStorage.getItem('blocks') || '{}')
|
currentPage: jsonData.pages[route.value.path]?.blocks
|
||||||
})
|
})
|
||||||
|
// 如果当前页面路由匹配不到,则重定向到首页
|
||||||
|
if (!state.currentPage) {
|
||||||
|
router.replace('/')
|
||||||
|
}
|
||||||
|
|
||||||
// 渲染组件
|
// 渲染组件
|
||||||
const renderCom = (element) => {
|
const renderCom = (element) => {
|
||||||
if (Array.isArray(element)) {
|
if (Array.isArray(element)) {
|
||||||
return element.map((item) => renderCom(item))
|
return element.map((item) => renderCom(item))
|
||||||
}
|
}
|
||||||
const component = props.config.componentMap[element.componentKey]
|
const component = visualConfig.componentMap[element.componentKey]
|
||||||
|
|
||||||
return component.render({
|
return component.render({
|
||||||
size: {},
|
size: {},
|
||||||
|
@ -54,11 +68,14 @@ export default defineComponent({
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.h5-preview {
|
.h5-preview {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.el-dialog__header {
|
.el-dialog__header {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simulator {
|
.simulator {
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
37
src/App.vue
37
src/App.vue
|
@ -1,44 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- <router-view #="{ Component }">-->
|
<visual-editor />
|
||||||
<!-- <component :is="Component" />-->
|
|
||||||
<!-- </router-view>-->
|
|
||||||
<visual-editor
|
|
||||||
v-model="jsonData"
|
|
||||||
:config="visualConfig"
|
|
||||||
:form-data="formData"
|
|
||||||
:custom-props="customProps"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, reactive, toRefs } from 'vue'
|
import { defineComponent, provide } from 'vue'
|
||||||
import VisualEditor from '@/visual-editor/index.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({
|
export default defineComponent({
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: { VisualEditor },
|
components: { VisualEditor },
|
||||||
setup() {
|
setup() {
|
||||||
const state = reactive({
|
const visualData = initVisualData()
|
||||||
jsonData: {
|
// 注入可视化编辑器所有配置
|
||||||
container: {
|
provide(injectKey, visualData)
|
||||||
height: 500,
|
|
||||||
width: 800
|
const { jsonData } = visualData
|
||||||
},
|
|
||||||
blocks: JSON.parse(sessionStorage.getItem('blocks') || '[]')
|
|
||||||
},
|
|
||||||
formData: [],
|
|
||||||
customProps: {}
|
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
sessionStorage.setItem('blocks', JSON.stringify(state.jsonData.blocks))
|
sessionStorage.setItem(localKey, JSON.stringify(jsonData))
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
visualConfig
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -27,23 +27,21 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, reactive, toRefs } from 'vue'
|
import { defineComponent, reactive, toRefs } from 'vue'
|
||||||
import Preview from './preview.vue'
|
import Preview from './preview.vue'
|
||||||
|
import { useVisualData, localKey } from '@/visual-editor/hooks/useVisualData'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Header',
|
name: 'Header',
|
||||||
components: { Preview },
|
components: { Preview },
|
||||||
props: {
|
setup() {
|
||||||
jsonData: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
isShowH5Preview: false
|
isShowH5Preview: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { jsonData } = useVisualData()
|
||||||
|
|
||||||
const runPreview = () => {
|
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
|
state.isShowH5Preview = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<el-dialog v-model="dialogVisible" custom-class="h5-preview" :show-close="false" width="360px">
|
<el-dialog v-model="dialogVisible" custom-class="h5-preview" :show-close="false" width="360px">
|
||||||
<iframe
|
<iframe
|
||||||
style="width: 360px; height: 640px"
|
style="width: 360px; height: 640px"
|
||||||
:src="`${BASE_URL}preview/#/`"
|
:src="previewUrl"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
scrolling="auto"
|
scrolling="auto"
|
||||||
></iframe>
|
></iframe>
|
||||||
|
@ -10,9 +10,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="tsx">
|
<script lang="tsx">
|
||||||
import { defineComponent, reactive, watch, toRefs } from 'vue'
|
import { defineComponent, reactive, toRefs } from 'vue'
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import { cloneDeep } from 'lodash'
|
|
||||||
import { BASE_URL } from '@/visual-editor/utils'
|
import { BASE_URL } from '@/visual-editor/utils'
|
||||||
/**
|
/**
|
||||||
* @name: preview
|
* @name: preview
|
||||||
|
@ -33,37 +32,11 @@ export default defineComponent({
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
dialogVisible: useVModel(props, 'visible', emit),
|
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 {
|
return {
|
||||||
...toRefs(state),
|
...toRefs(state)
|
||||||
BASE_URL,
|
|
||||||
renderCom
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,9 +1,24 @@
|
||||||
<!--页面树-->
|
<!--页面树-->
|
||||||
<template>
|
<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 }">
|
<template #default="{ node, data }">
|
||||||
<span class="custom-tree-node">
|
<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>
|
<span @click.stop>
|
||||||
<el-dropdown trigger="click">
|
<el-dropdown trigger="click">
|
||||||
<span class="el-dropdown-link">
|
<span class="el-dropdown-link">
|
||||||
|
@ -11,12 +26,15 @@
|
||||||
</span>
|
</span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item icon="el-icon-plus" @click="addPage(data)"
|
<el-dropdown-item icon="el-icon-edit" @click="editPage(data)"
|
||||||
>添加子页面</el-dropdown-item
|
>编辑</el-dropdown-item
|
||||||
>
|
>
|
||||||
<el-dropdown-item icon="el-icon-delete" @click="delPage(data)"
|
<el-dropdown-item icon="el-icon-delete" @click="delPage(data)"
|
||||||
>删除</el-dropdown-item
|
>删除</el-dropdown-item
|
||||||
>
|
>
|
||||||
|
<el-dropdown-item icon="el-icon-link" @click="setDefaultPage(data)"
|
||||||
|
>设为默认</el-dropdown-item
|
||||||
|
>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
|
@ -24,39 +42,119 @@
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-tree>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, reactive, toRefs } from 'vue'
|
import { defineComponent, reactive, computed, toRefs } from 'vue'
|
||||||
import { treeData } from './treeData'
|
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'PageTree',
|
name: 'PageTree',
|
||||||
setup() {
|
setup() {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const { jsonData, setCurrentPage, deletePage, updatePage, incrementPage } = useVisualData()
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
data: treeData,
|
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
children: 'children',
|
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) => {
|
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)
|
console.log('子页面数据:', data)
|
||||||
}
|
}
|
||||||
// 删除子页面
|
// 删除子页面
|
||||||
const delPage = (data) => {
|
const delPage = (data) => {
|
||||||
console.log('删除子页面数据', 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 {
|
return {
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
|
setCurrentPage,
|
||||||
|
pages,
|
||||||
|
onSubmit,
|
||||||
|
setDefaultPage,
|
||||||
handleNodeClick,
|
handleNodeClick,
|
||||||
addPage,
|
addPage,
|
||||||
|
editPage,
|
||||||
delPage
|
delPage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +1,7 @@
|
||||||
export const treeData = [
|
export const treeData = [
|
||||||
{
|
{
|
||||||
label: '一级 1',
|
title: '首页',
|
||||||
children: [
|
path: '/',
|
||||||
{
|
isDefault: true
|
||||||
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'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
{{ tabItem.label }}
|
{{ tabItem.label }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<component :is="tabItem.componentName" />
|
<component :is="tabItem.componentName" v-bind="$attrs" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</template>
|
</template>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { defineComponent, PropType } from 'vue'
|
import { defineComponent, PropType } from 'vue'
|
||||||
import { VisualEditorProps } from '../../../../visual-editor.props'
|
import { VisualEditorProps } from '../../../../visual-editor.props'
|
||||||
import { useModel } from '../../../../utils/useModel'
|
import { useModel } from '../../../../hooks/useModel'
|
||||||
import { ElButton, ElTag } from 'element-plus'
|
import { ElButton, ElTag } from 'element-plus'
|
||||||
import { $$tablePropEditor } from './table-prop-edit.service'
|
import { $$tablePropEditor } from './table-prop-edit.service'
|
||||||
|
|
||||||
|
|
|
@ -23,23 +23,20 @@ import {
|
||||||
} from 'element-plus'
|
} from 'element-plus'
|
||||||
import { VisualEditorProps, VisualEditorPropsType } from '@/visual-editor/visual-editor.props'
|
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 { TablePropEditor } from '@/visual-editor/components/right-attribute-panel/components/table-prop-editor/table-prop-editor'
|
||||||
import {
|
import { VisualEditorBlockData } from '@/visual-editor/visual-editor.utils'
|
||||||
VisualEditorBlockData,
|
|
||||||
VisualEditorConfig,
|
|
||||||
VisualEditorModelValue
|
|
||||||
} from '@/visual-editor/visual-editor.utils'
|
|
||||||
import MonacoEditor from './MonacoEditor'
|
import MonacoEditor from './MonacoEditor'
|
||||||
import { useVModel } from '@vueuse/core'
|
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({
|
export default defineComponent({
|
||||||
name: 'RightAttributePanel',
|
name: 'RightAttributePanel',
|
||||||
props: {
|
props: {
|
||||||
block: { type: Object as PropType<VisualEditorBlockData>, default: () => ({}) },
|
block: { type: Object as PropType<VisualEditorBlockData>, default: () => ({}) }
|
||||||
config: { type: Object as PropType<VisualEditorConfig>, required: true },
|
|
||||||
dataModel: { type: Object as PropType<{ value: VisualEditorModelValue }>, required: true }
|
|
||||||
},
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
|
const { visualConfig } = useVisualData()
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
activeName: 'attr',
|
activeName: 'attr',
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
|
@ -85,7 +82,7 @@ export default defineComponent({
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const { componentKey } = props.block
|
const { componentKey } = props.block
|
||||||
const component = props.config.componentMap[componentKey]
|
const component = visualConfig.componentMap[componentKey]
|
||||||
console.log('props.block:', props.block)
|
console.log('props.block:', props.block)
|
||||||
content.push(
|
content.push(
|
||||||
<ElFormItem label="组件ID" labelWidth={'76px'}>
|
<ElFormItem label="组件ID" labelWidth={'76px'}>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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 }">
|
<template #item="{ element: outElement }">
|
||||||
<div
|
<div
|
||||||
class="list-group-item"
|
class="list-group-item"
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
@mousedown="selectComp(outElement)"
|
@mousedown="selectComp(outElement)"
|
||||||
>
|
>
|
||||||
<comp-render
|
<comp-render
|
||||||
:config="config"
|
:config="visualConfig"
|
||||||
:element="outElement"
|
:element="outElement"
|
||||||
:style="{
|
:style="{
|
||||||
pointerEvents: Object.keys(outElement.props?.slots || {}).length ? 'auto' : 'none'
|
pointerEvents: Object.keys(outElement.props?.slots || {}).length ? 'auto' : 'none'
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
<slot-item
|
<slot-item
|
||||||
v-model:children="value.children"
|
v-model:children="value.children"
|
||||||
:slot-key="slotKey"
|
:slot-key="slotKey"
|
||||||
:config="config"
|
:config="visualConfig"
|
||||||
:on-contextmenu-block="onContextmenuBlock"
|
:on-contextmenu-block="onContextmenuBlock"
|
||||||
:select-comp="selectComp"
|
:select-comp="selectComp"
|
||||||
/>
|
/>
|
||||||
|
@ -36,15 +36,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="tsx">
|
<script lang="tsx">
|
||||||
import { defineComponent, reactive, toRefs, Ref, PropType, SetupContext } from 'vue'
|
import { defineComponent, reactive, toRefs, SetupContext } from 'vue'
|
||||||
import { VisualEditorConfig, VisualEditorBlockData } from '@/visual-editor/visual-editor.utils'
|
import { VisualEditorBlockData } from '@/visual-editor/visual-editor.utils'
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import DraggableTransitionGroup from './draggable-transition-group.vue'
|
import DraggableTransitionGroup from './draggable-transition-group.vue'
|
||||||
import { $$dropdown, DropdownOption } from '@/visual-editor/utils/dropdown-service'
|
import { $$dropdown, DropdownOption } from '@/visual-editor/utils/dropdown-service'
|
||||||
import CompRender from './comp-render'
|
import CompRender from './comp-render'
|
||||||
import SlotItem from './slot-item.vue'
|
import SlotItem from './slot-item.vue'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
|
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
|
||||||
|
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SimulatorEditor',
|
name: 'SimulatorEditor',
|
||||||
|
@ -53,18 +53,15 @@ export default defineComponent({
|
||||||
CompRender,
|
CompRender,
|
||||||
SlotItem
|
SlotItem
|
||||||
},
|
},
|
||||||
props: {
|
emits: ['on-selected'],
|
||||||
blocks: { type: Array as PropType<VisualEditorBlockData[]>, required: true },
|
setup(_, { emit }: SetupContext) {
|
||||||
config: { type: Object as PropType<VisualEditorConfig>, required: true }
|
|
||||||
},
|
|
||||||
emits: ['update:blocks', 'on-selected'],
|
|
||||||
setup(props, { emit }: SetupContext) {
|
|
||||||
const { globalProperties } = useGlobalProperties()
|
const { globalProperties } = useGlobalProperties()
|
||||||
|
|
||||||
|
const { currentPage, visualConfig } = useVisualData()
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
compRefs: [],
|
compRefs: [],
|
||||||
drag: false,
|
drag: false
|
||||||
VMBlocks: useVModel(props, 'blocks', emit) as Ref<VisualEditorBlockData[]>
|
|
||||||
})
|
})
|
||||||
|
|
||||||
//递归实现
|
//递归实现
|
||||||
|
@ -104,7 +101,7 @@ export default defineComponent({
|
||||||
item.focusWithChild = false
|
item.focusWithChild = false
|
||||||
item.focus = item._vid == _vid
|
item.focus = item._vid == _vid
|
||||||
if (item.focus) {
|
if (item.focus) {
|
||||||
const arr = findPathByLeafId(_vid, state.VMBlocks)
|
const arr = findPathByLeafId(_vid, currentPage.value.blocks)
|
||||||
arr.forEach((n) => (n.focusWithChild = true))
|
arr.forEach((n) => (n.focusWithChild = true))
|
||||||
}
|
}
|
||||||
if (Object.keys(item.props?.slots || {}).length) {
|
if (Object.keys(item.props?.slots || {}).length) {
|
||||||
|
@ -117,7 +114,7 @@ export default defineComponent({
|
||||||
|
|
||||||
const selectComp = (element) => {
|
const selectComp = (element) => {
|
||||||
emit('on-selected', element)
|
emit('on-selected', element)
|
||||||
state.VMBlocks.forEach((block) => {
|
currentPage.value.blocks.forEach((block) => {
|
||||||
block.focus = element._vid == block._vid
|
block.focus = element._vid == block._vid
|
||||||
block.focusWithChild = false
|
block.focusWithChild = false
|
||||||
handleSlotsFocus(block, element._vid)
|
handleSlotsFocus(block, element._vid)
|
||||||
|
@ -128,7 +125,7 @@ export default defineComponent({
|
||||||
const onContextmenuBlock = (
|
const onContextmenuBlock = (
|
||||||
e: MouseEvent,
|
e: MouseEvent,
|
||||||
block: VisualEditorBlockData,
|
block: VisualEditorBlockData,
|
||||||
parentBlocks = state.VMBlocks
|
parentBlocks = currentPage.value.blocks
|
||||||
) => {
|
) => {
|
||||||
$$dropdown({
|
$$dropdown({
|
||||||
reference: e,
|
reference: e,
|
||||||
|
@ -180,6 +177,8 @@ export default defineComponent({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
|
currentPage,
|
||||||
|
visualConfig,
|
||||||
selectComp,
|
selectComp,
|
||||||
onContextmenuBlock
|
onContextmenuBlock
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
/**
|
||||||
|
* @name: useVisualData
|
||||||
|
* @author: 卜启缘
|
||||||
|
* @date: 2021/5/6 11:59
|
||||||
|
* @description:useVisualData
|
||||||
|
* @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)!
|
|
@ -2,7 +2,7 @@
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header height="80px" class="flex items-center shadow-md">
|
<el-header height="80px" class="flex items-center shadow-md">
|
||||||
<!-- 顶部start -->
|
<!-- 顶部start -->
|
||||||
<Header :json-data="modelValue" :config="config" />
|
<Header />
|
||||||
<!-- 顶部end -->
|
<!-- 顶部end -->
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-container class="layout-container">
|
<el-container class="layout-container">
|
||||||
|
@ -14,20 +14,12 @@
|
||||||
<el-main>
|
<el-main>
|
||||||
<!-- 中间编辑区域start -->
|
<!-- 中间编辑区域start -->
|
||||||
<simulator>
|
<simulator>
|
||||||
<simulator-editor
|
<simulator-editor @on-selected="onSelected" />
|
||||||
v-model:blocks="dataModel.blocks"
|
|
||||||
:config="config"
|
|
||||||
@on-selected="onSelected"
|
|
||||||
/>
|
|
||||||
</simulator>
|
</simulator>
|
||||||
<!-- 中间编辑区域end -->
|
<!-- 中间编辑区域end -->
|
||||||
|
|
||||||
<!-- 右侧属性面板start -->
|
<!-- 右侧属性面板start -->
|
||||||
<right-attribute-panel
|
<right-attribute-panel v-model:block="currentBlock" />
|
||||||
v-model:block="currentBlock"
|
|
||||||
:config="config"
|
|
||||||
:data-model="dataModel"
|
|
||||||
/>
|
|
||||||
<!-- 右侧属性面板end -->
|
<!-- 右侧属性面板end -->
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
|
@ -35,37 +27,23 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType, reactive, toRefs } from 'vue'
|
import { defineComponent, reactive, toRefs } from 'vue'
|
||||||
import Header from './components/header/index.vue'
|
import Header from './components/header/index.vue'
|
||||||
import LeftAside from './components/left-aside/index.vue'
|
import LeftAside from './components/left-aside/index.vue'
|
||||||
import RightAttributePanel from './components/right-attribute-panel'
|
import RightAttributePanel from './components/right-attribute-panel'
|
||||||
import SimulatorEditor from './components/simulator-editor/simulator-editor.vue'
|
import SimulatorEditor from './components/simulator-editor/simulator-editor.vue'
|
||||||
import Simulator from './components/common/simulator.vue'
|
import Simulator from './components/common/simulator.vue'
|
||||||
import {
|
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
|
||||||
VisualEditorConfig,
|
|
||||||
VisualEditorModelValue,
|
|
||||||
VisualEditorComponent
|
|
||||||
} from '@/visual-editor/visual-editor.utils'
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
dataModel: VisualEditorModelValue
|
|
||||||
currentBlock: VisualEditorComponent
|
currentBlock: VisualEditorComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Layout',
|
name: 'Layout',
|
||||||
components: { Header, LeftAside, RightAttributePanel, SimulatorEditor, Simulator },
|
components: { Header, LeftAside, RightAttributePanel, SimulatorEditor, Simulator },
|
||||||
props: {
|
setup() {
|
||||||
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) {
|
|
||||||
const state: IState = reactive({
|
const state: IState = reactive({
|
||||||
dataModel: useVModel(props, 'modelValue'),
|
|
||||||
currentBlock: {} as VisualEditorComponent
|
currentBlock: {} as VisualEditorComponent
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { VisualEditorProps } from './visual-editor.props'
|
import { VisualEditorProps } from './visual-editor.props'
|
||||||
import { inject, provide } from 'vue'
|
import { inject, provide } from 'vue'
|
||||||
import { useDotProp } from '@/visual-editor/utils/useDotProp'
|
import { useDotProp } from '@/visual-editor/hooks/useDotProp'
|
||||||
|
|
||||||
export interface VisualEditorBlockData {
|
export interface VisualEditorBlockData {
|
||||||
_vid: string // 组件id 时间戳
|
_vid: string // 组件id 时间戳
|
||||||
|
@ -21,12 +21,23 @@ export interface VisualEditorBlockData {
|
||||||
[prop: string]: any
|
[prop: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VisualEditorPage {
|
||||||
|
title: string // 页面标题
|
||||||
|
isDefault?: boolean // 404是重定向到默认页面
|
||||||
|
blocks: VisualEditorBlockData[] // 当前页面的所有组件
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VisualEditorPages {
|
||||||
|
[path: string]: VisualEditorPage
|
||||||
|
}
|
||||||
|
|
||||||
export interface VisualEditorModelValue {
|
export interface VisualEditorModelValue {
|
||||||
container: {
|
container: {
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
}
|
}
|
||||||
blocks?: VisualEditorBlockData[]
|
// 页面
|
||||||
|
pages: VisualEditorPages
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VisualEditorComponent {
|
export interface VisualEditorComponent {
|
||||||
|
|
Loading…
Reference in New Issue