feat: 添加页面
This commit is contained in:
parent
ea986f3e6e
commit
6471c17ac7
56
.eslintrc.js
56
.eslintrc.js
|
@ -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: {}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
37
src/App.vue
37
src/App.vue
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'}>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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-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
|
||||
})
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue