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: { 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: {}
} }

View File

@ -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>

View File

@ -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;
} }

View File

@ -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>

View File

@ -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
} }

View File

@ -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
} }
} }
}) })

View File

@ -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
} }
} }

View File

@ -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'
}
]
}
]
} }
] ]

View File

@ -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>

View File

@ -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'

View File

@ -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'}>

View File

@ -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
} }

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-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
}) })

View File

@ -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 {