feat: add canvas route bar (#967)
* feat: add route link bar above the canvas * feat: route bar support switch route * add TODO * change css variables * fix: fix switch page error after create one * feat: support select router page on RouterLink * feat: RouterLink support navigate by right-click menu operation in canvas * feat: navigation operation place to first * feat: support setting default page * fix: hide route bar in block canvas * fix: hide route bar in block canvas * feat: add confirm modal before switch page * fix: remove TODO comment * fix: Modified based on review comments * fix some issues * fix wrong type of route id
This commit is contained in:
parent
170fd41b9c
commit
3bf93ffb55
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<component :is="CanvasLayout">
|
||||
<template #header>
|
||||
<component v-if="!isBlock()" :is="CanvasRouteBar"></component>
|
||||
</template>
|
||||
<template #container>
|
||||
<component
|
||||
:is="CanvasContainer.entry"
|
||||
|
|
@ -52,7 +55,7 @@ export default {
|
|||
setup() {
|
||||
const registry = getMergeRegistry('canvas')
|
||||
const materialsPanel = getMergeMeta('engine.plugins.materials')?.entry
|
||||
const { CanvasBreadcrumb } = registry.components
|
||||
const { CanvasRouteBar, CanvasBreadcrumb } = registry.components
|
||||
const CanvasLayout = registry.layout.entry
|
||||
const [CanvasContainer] = registry.metas
|
||||
const footData = ref([])
|
||||
|
|
@ -74,6 +77,8 @@ export default {
|
|||
pageState.properties = null
|
||||
}
|
||||
|
||||
const isBlock = useCanvas().isBlock
|
||||
|
||||
watch(
|
||||
[() => useCanvas().isSaved(), () => useLayout().layoutState.pageStatus, () => useCanvas().getPageSchema()],
|
||||
([isSaved, pageStatus, pageSchema], [oldIsSaved, _oldPageStatus, oldPageSchema]) => {
|
||||
|
|
@ -221,9 +226,11 @@ export default {
|
|||
addHistoryDataChangedCallback,
|
||||
ast
|
||||
},
|
||||
isBlock,
|
||||
CanvasLayout,
|
||||
canvasRef,
|
||||
CanvasContainer,
|
||||
CanvasRouteBar,
|
||||
CanvasBreadcrumb
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div v-show="menuState.show" ref="menuDom" class="context-menu" :style="menuState.position">
|
||||
<ul class="menu-item">
|
||||
<li
|
||||
v-for="(item, index) in menus"
|
||||
v-for="(item, index) in filteredMenus"
|
||||
:key="index"
|
||||
:class="{
|
||||
'li-item': item.items,
|
||||
|
|
@ -32,9 +32,9 @@
|
|||
</template>
|
||||
|
||||
<script lang="jsx">
|
||||
import { ref, reactive, nextTick } from 'vue'
|
||||
import { ref, reactive, nextTick, computed } from 'vue'
|
||||
import { canvasState, getConfigure, getController, getCurrent, copyNode, removeNodeById } from '../container'
|
||||
import { useLayout, useModal, useCanvas, getMergeMeta } from '@opentiny/tiny-engine-meta-register'
|
||||
import { useLayout, useModal, useCanvas, usePage, getMergeMeta } from '@opentiny/tiny-engine-meta-register'
|
||||
import { iconRight } from '@opentiny/vue-icon'
|
||||
|
||||
const menuState = reactive({
|
||||
|
|
@ -121,6 +121,25 @@ export default {
|
|||
menus.value.push({ name: '新建区块', code: 'createBlock' })
|
||||
}
|
||||
|
||||
menus.value.unshift({
|
||||
name: '路由跳转',
|
||||
code: 'route',
|
||||
show: () => getCurrent()?.schema?.componentName === 'RouterLink',
|
||||
check: () => {
|
||||
const targetPageId = getCurrent().schema.props?.to?.name
|
||||
return typeof targetPageId === 'number' || targetPageId
|
||||
}
|
||||
})
|
||||
|
||||
const filteredMenus = computed(() =>
|
||||
menus.value.filter((item) => {
|
||||
if (typeof item.show === 'function') {
|
||||
return item.show()
|
||||
}
|
||||
return true
|
||||
})
|
||||
)
|
||||
|
||||
const boxVisibility = ref(false)
|
||||
|
||||
// 计算上下文菜单位置,右键时显示,否则关闭
|
||||
|
|
@ -205,10 +224,19 @@ export default {
|
|||
status: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
route() {
|
||||
// check中验证过了 targetPageId 是有效值
|
||||
const targetPageId = getCurrent().schema.props.to.name
|
||||
usePage().switchPageWithConfirm(targetPageId)
|
||||
}
|
||||
}
|
||||
|
||||
const actionDisabled = (actionItem) => {
|
||||
if (typeof actionItem.check === 'function' && !actionItem.check()) {
|
||||
return true
|
||||
}
|
||||
|
||||
const actions = ['del', 'copy', 'addParent']
|
||||
return actions.includes(actionItem.code) && !getCurrent().schema?.id
|
||||
}
|
||||
|
|
@ -221,7 +249,7 @@ export default {
|
|||
boxVisibility.value = false
|
||||
}
|
||||
const doOperation = (item) => {
|
||||
if ((item.check && !item.check?.()) || actionDisabled(item)) {
|
||||
if (actionDisabled(item)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +262,7 @@ export default {
|
|||
return {
|
||||
SaveNewBlock,
|
||||
menuState,
|
||||
menus,
|
||||
filteredMenus,
|
||||
doOperation,
|
||||
boxVisibility,
|
||||
close,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
|
||||
*
|
||||
*/
|
||||
import { CanvasRouteBar } from './route-bar'
|
||||
import { CanvasBreadcrumb } from './breadcrumb'
|
||||
|
||||
// meta app
|
||||
|
|
@ -21,6 +22,7 @@ export { CanvasContainer, CanvasLayout, DesignCanvas }
|
|||
export default {
|
||||
...DesignCanvas,
|
||||
components: {
|
||||
CanvasRouteBar,
|
||||
CanvasBreadcrumb
|
||||
},
|
||||
layout: CanvasLayout,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div id="canvas-wrap" ref="canvasRef">
|
||||
<slot name="header"></slot>
|
||||
<div ref="siteCanvas" class="site-canvas" :style="siteCanvasStyle">
|
||||
<slot name="container"></slot>
|
||||
</div>
|
||||
|
|
@ -8,13 +9,20 @@
|
|||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useLayout } from '@opentiny/tiny-engine-meta-register'
|
||||
import { useCanvas, useLayout } from '@opentiny/tiny-engine-meta-register'
|
||||
|
||||
const ROUTE_BAR_HEIGHT = 32
|
||||
|
||||
const { isBlock } = useCanvas()
|
||||
const dimension = useLayout().getDimension()
|
||||
|
||||
const siteCanvasStyle = computed(() => {
|
||||
const { scale } = useLayout().getDimension()
|
||||
const { scale } = dimension
|
||||
const routeBarHeight = isBlock() ? 0 : ROUTE_BAR_HEIGHT
|
||||
return {
|
||||
height: `calc((100% - var(--base-bottom-panel-height, 30px) - 36px) / ${scale})`,
|
||||
transform: `scale(${scale})`
|
||||
height: `calc((100% - var(--base-bottom-panel-height, 30px) - ${36 + routeBarHeight}px) / ${scale})`,
|
||||
transform: `scale(${scale})`,
|
||||
marginTop: `${18 + routeBarHeight}px`
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -32,7 +40,7 @@ const siteCanvasStyle = computed(() => {
|
|||
background: var(--ti-lowcode-breadcrumb-hover-bg);
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
margin: 18px 0;
|
||||
margin-bottom: 18px;
|
||||
transform-origin: top;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
</span>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import { computed, inject, PropType, Ref } from 'vue'
|
||||
export default {
|
||||
props: {
|
||||
activeClass: {
|
||||
|
|
@ -22,17 +22,30 @@ export default {
|
|||
default: ''
|
||||
},
|
||||
to: {
|
||||
type: String
|
||||
// TODO: 待改成页面选择器
|
||||
// type: Object as PropType<{
|
||||
// pageId: string
|
||||
// }>
|
||||
// TODO: 支持绝对路径,类型为String
|
||||
type: Object as PropType<{
|
||||
name: string
|
||||
}>
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const pageAncestor = (inject('page-ancestors') as Ref<string[] | null>).value
|
||||
const active = computed(() => pageAncestor?.length && pageAncestor.indexOf(props.to) > -1)
|
||||
const exactActive = computed(() => pageAncestor?.length && props.to === pageAncestor[pageAncestor.length - 1])
|
||||
const active = computed(() => {
|
||||
if (!Array.isArray(pageAncestor) || !props.to?.name) {
|
||||
return false
|
||||
}
|
||||
|
||||
return pageAncestor.includes(props.to.name)
|
||||
})
|
||||
|
||||
const exactActive = computed(() => {
|
||||
if (!Array.isArray(pageAncestor) || !props.to?.name) {
|
||||
return false
|
||||
}
|
||||
|
||||
return props.to.name === pageAncestor[pageAncestor.length - 1]
|
||||
})
|
||||
|
||||
return {
|
||||
active,
|
||||
exactActive
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@
|
|||
},
|
||||
"cols": 12,
|
||||
"widget": {
|
||||
"component": "InputConfigurator",
|
||||
"component": "RouterSelectConfigurator",
|
||||
"props": {}
|
||||
}
|
||||
},
|
||||
|
|
@ -630,9 +630,7 @@
|
|||
"icon": "RouterLink",
|
||||
"schema": {
|
||||
"componentName": "RouterLink",
|
||||
"props": {
|
||||
"to": ""
|
||||
},
|
||||
"props": {},
|
||||
"children": [
|
||||
{
|
||||
"componentName": "Text",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export { default as CanvasRouteBar } from './src/CanvasRouteBar.vue'
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<div id="canvas-route-bar" :style="sizeStyle">
|
||||
<div class="address-bar">
|
||||
<template v-for="route in routes" :key="route.id">
|
||||
<span class="slash">/</span>
|
||||
<span :class="[{ route: route.isPage && route.id !== pageId }]" @click="handleClickRoute(route)">{{
|
||||
route.route
|
||||
}}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getMetaApi, META_SERVICE, useLayout, useMessage, usePage } from '@opentiny/tiny-engine-meta-register'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const sizeStyle = computed(() => {
|
||||
const { width } = useLayout().getDimension()
|
||||
return { width }
|
||||
})
|
||||
|
||||
const { pageSettingState, getAncestors, switchPageWithConfirm } = usePage()
|
||||
|
||||
const pageId = ref(getMetaApi(META_SERVICE.GlobalService).getBaseInfo().pageId)
|
||||
|
||||
const { subscribe, unsubscribe } = useMessage()
|
||||
|
||||
let subscriber = null
|
||||
|
||||
onMounted(() => {
|
||||
subscriber = subscribe({
|
||||
topic: 'locationHistoryChanged',
|
||||
callback: (data) => {
|
||||
if (data.pageId) {
|
||||
pageId.value = data.pageId
|
||||
}
|
||||
},
|
||||
subscriber: 'routeBar'
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (subscriber) {
|
||||
unsubscribe(subscriber)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @typedef {Object} Route
|
||||
* @property {string | number} id
|
||||
* @property {string} route
|
||||
* @property {boolean} isPage
|
||||
*/
|
||||
|
||||
/** @type {import('vue').Ref<Route[]>} */
|
||||
const routes = ref([])
|
||||
|
||||
watch(
|
||||
pageId,
|
||||
async (value) => {
|
||||
if (!value) {
|
||||
routes.value = []
|
||||
return
|
||||
}
|
||||
const ancestors = await getAncestors(value, true)
|
||||
|
||||
routes.value = ancestors.concat(value).map((id) => {
|
||||
const { route, isPage } = pageSettingState.treeDataMapping[id]
|
||||
return {
|
||||
id,
|
||||
route: route
|
||||
.replace(/\/+/g, '/') // 替换连续的 '/' 为单个 '/'
|
||||
.replace(/^\/|\/$/g, ''), // 去掉开头和结尾的 '/'
|
||||
isPage
|
||||
}
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/**
|
||||
* @param route {Route}
|
||||
*/
|
||||
const handleClickRoute = (route) => {
|
||||
switchPageWithConfirm(route.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#canvas-route-bar {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
height: 32px;
|
||||
max-width: 100%;
|
||||
background-color: var(--te-common-bg-prompt);
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.address-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
background-color: var(--te-common-bg-container);
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
border-radius: 999px;
|
||||
padding: 0 10px;
|
||||
cursor: default;
|
||||
}
|
||||
.route {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--te-common-text-link);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -20,6 +20,7 @@ import RadioConfigurator from './radio-configurator/RadioConfigurator.vue'
|
|||
import RadioGroupConfigurator from './radio-group-configurator/RadioGroupConfigurator.vue'
|
||||
import RelatedColumnsConfigurator from './related-columns-configurator/RelatedColumnsConfigurator.vue'
|
||||
import RelatedEditorConfigurator from './related-editor-configurator/RelatedEditorConfigurator.vue'
|
||||
import RouterSelectConfigurator from './router-select-configurator/RouterSelectConfigurator.vue'
|
||||
import SelectConfigurator from './select-configurator/SelectConfigurator.vue'
|
||||
import SelectIconConfigurator from './select-icon-configurator/SelectIconConfigurator.vue'
|
||||
import SliderConfigurator from './slider-configurator/SliderConfigurator.vue'
|
||||
|
|
@ -54,6 +55,7 @@ export {
|
|||
RadioGroupConfigurator,
|
||||
RelatedColumnsConfigurator,
|
||||
RelatedEditorConfigurator,
|
||||
RouterSelectConfigurator,
|
||||
SelectConfigurator,
|
||||
SelectIconConfigurator,
|
||||
SliderConfigurator,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
<template>
|
||||
<tiny-select
|
||||
v-model="state.selected"
|
||||
value-field="id"
|
||||
render-type="tree"
|
||||
:tree-op="treeFolderOp"
|
||||
text-field="name"
|
||||
:clearable="true"
|
||||
popper-class="page-tree-select-dropdown"
|
||||
@change="handleChange"
|
||||
>
|
||||
</tiny-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="jsx">
|
||||
import { usePage } from '@opentiny/tiny-engine-meta-register'
|
||||
import { Select as TinySelect } from '@opentiny/vue'
|
||||
import { computed, defineEmits, defineProps, reactive, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Array],
|
||||
default: () => ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const state = reactive({
|
||||
selected: props.modelValue?.name ?? ''
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue?.name,
|
||||
(value) => {
|
||||
state.selected = value ?? ''
|
||||
}
|
||||
)
|
||||
|
||||
const { pageSettingState, getPageList, STATIC_PAGE_GROUP_ID } = usePage()
|
||||
|
||||
const pages = computed(() => pageSettingState.pages[STATIC_PAGE_GROUP_ID].data)
|
||||
|
||||
if (!Array.isArray(pages.value)) {
|
||||
getPageList()
|
||||
}
|
||||
|
||||
const pageToTreeData = (page) => {
|
||||
const { id, name, isPage, children } = page
|
||||
|
||||
// id 可能为数字,需要转换成字符串
|
||||
const result = { id: String(id), name, isPage, disabled: !isPage }
|
||||
|
||||
if (Array.isArray(children)) {
|
||||
result.children = children.map((page) => pageToTreeData(page))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const getNodeIcon = (data) => {
|
||||
if (data.id === pageSettingState.ROOT_ID) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (data.isPage) {
|
||||
return <SvgIcon name="text-page-common"></SvgIcon>
|
||||
}
|
||||
|
||||
return <SvgIcon name="text-page-folder"></SvgIcon>
|
||||
}
|
||||
|
||||
const treeFolderOp = computed(() => {
|
||||
const dummyRoot = pageToTreeData({ children: pages.value })
|
||||
const data = dummyRoot.children
|
||||
const options = {
|
||||
data: data,
|
||||
shrinkIcon: null,
|
||||
expandIcon: null,
|
||||
renderContent: (_h, { node, data }) => {
|
||||
return (
|
||||
<>
|
||||
{getNodeIcon(data)}
|
||||
<div>{node.label}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
const handleChange = () => {
|
||||
emit('update:modelValue', { name: state.selected })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.tiny-select-dropdown.page-tree-select-dropdown {
|
||||
padding: 8px 0;
|
||||
|
||||
.tiny-tree .tiny-tree-node__wrapper .tiny-tree-node {
|
||||
.tiny-tree-node__content {
|
||||
padding: 0;
|
||||
background-color: var(--te-common-bg-default);
|
||||
&:hover {
|
||||
background-color: var(--te-common-bg-container);
|
||||
}
|
||||
// 移除子节点的的背景色,才能保证鼠标hover到.tiny-tree-node__content节点任意位置时,整行都有hover状态的背景色
|
||||
.tiny-tree-node__content-left,
|
||||
.tiny-tree-node__content-left .tiny-tree-node__content-box {
|
||||
background-color: unset;
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
.tiny-tree-node__content-left {
|
||||
padding: 0;
|
||||
.tree-node-icon {
|
||||
margin: 0;
|
||||
}
|
||||
.tiny-tree-node__content-box {
|
||||
padding: 0 12px;
|
||||
svg {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
.tiny-tree-node__label {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.is-disabled > .tiny-tree-node__content .tiny-tree-node__content-box {
|
||||
color: var(--te-common-text-disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -53,6 +53,10 @@
|
|||
</span>
|
||||
</div>
|
||||
</tiny-form-item>
|
||||
|
||||
<tiny-form-item v-if="pageSettingState.currentPageData.group !== 'publicPages'" prop="isDefault">
|
||||
<tiny-checkbox v-model="pageSettingState.currentPageData.isDefault">设为默认页</tiny-checkbox>
|
||||
</tiny-form-item>
|
||||
</tiny-form>
|
||||
<page-home
|
||||
v-if="!isFolder && !pageSettingState.isNew && pageSettingState.currentPageData.group !== 'public'"
|
||||
|
|
@ -62,7 +66,7 @@
|
|||
|
||||
<script lang="jsx">
|
||||
import { ref, computed, watchEffect } from 'vue'
|
||||
import { Form, FormItem, Input, Select, Radio } from '@opentiny/vue'
|
||||
import { Form, FormItem, Input, Select, Radio, Checkbox } from '@opentiny/vue'
|
||||
import { usePage } from '@opentiny/tiny-engine-meta-register'
|
||||
import { REGEXP_PAGE_NAME, REGEXP_FOLDER_NAME, REGEXP_ROUTE } from '@opentiny/tiny-engine-common/js/verification'
|
||||
import PageHome from './PageHome.vue'
|
||||
|
|
@ -74,7 +78,8 @@ export default {
|
|||
TinyInput: Input,
|
||||
TinySelect: Select,
|
||||
PageHome,
|
||||
TinyRadio: Radio
|
||||
TinyRadio: Radio,
|
||||
TinyCheckbox: Checkbox
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
|
|
@ -199,7 +204,7 @@ export default {
|
|||
return (
|
||||
<>
|
||||
{getNodeIcon(data)}
|
||||
<label>{node.label}</label>
|
||||
<div>{node.label}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -319,9 +324,6 @@ export default {
|
|||
svg {
|
||||
margin-right: 8px;
|
||||
}
|
||||
* {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.tiny-tree-node__label {
|
||||
font-size: 12px;
|
||||
|
|
|
|||
|
|
@ -55,15 +55,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="jsx">
|
||||
import { reactive, onUnmounted } from 'vue'
|
||||
import { reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { Search, Collapse, CollapseItem, Popover } from '@opentiny/vue'
|
||||
import { IconFolderOpened, IconFolderClosed, IconSearch } from '@opentiny/vue-icon'
|
||||
import {
|
||||
useCanvas,
|
||||
useModal,
|
||||
usePage,
|
||||
useBreadcrumb,
|
||||
useLayout,
|
||||
useNotify,
|
||||
useMessage,
|
||||
getMetaApi,
|
||||
|
|
@ -78,7 +76,7 @@ import { closeFolderSettingPanel } from './PageFolderSetting.vue'
|
|||
import http from './http.js'
|
||||
import DraggbleTree from './Tree.vue'
|
||||
|
||||
const { PAGE_STATUS, COMPONENT_NAME } = constants
|
||||
const { PAGE_STATUS } = constants
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -98,7 +96,7 @@ export default {
|
|||
emits: ['openSettingPanel', 'add', 'createPage', 'createFolder'],
|
||||
setup(props, { emit }) {
|
||||
const { confirm } = useModal()
|
||||
const { initData, pageState, isBlock, isSaved } = useCanvas()
|
||||
const { pageState, isBlock, isSaved } = useCanvas()
|
||||
const {
|
||||
pageSettingState,
|
||||
changeTreeData,
|
||||
|
|
@ -107,10 +105,9 @@ export default {
|
|||
resetPageData,
|
||||
STATIC_PAGE_GROUP_ID,
|
||||
COMMON_PAGE_GROUP_ID,
|
||||
postLocationHistoryChanged
|
||||
switchPage: switchPageById
|
||||
} = usePage()
|
||||
const { fetchPageDetail, requestUpdatePage } = http
|
||||
const { setBreadcrumbPage } = useBreadcrumb()
|
||||
const getAppId = () => getMetaApi(META_SERVICE.GlobalService).getBaseInfo().id
|
||||
|
||||
const state = reactive({
|
||||
|
|
@ -119,6 +116,28 @@ export default {
|
|||
currentNodeData: { id: getMetaApi(META_SERVICE.GlobalService).getBaseInfo().pageId }
|
||||
})
|
||||
|
||||
const { subscribe, unsubscribe } = useMessage()
|
||||
|
||||
let subscriber = null
|
||||
|
||||
onMounted(() => {
|
||||
subscriber = subscribe({
|
||||
topic: 'locationHistoryChanged',
|
||||
callback: (data) => {
|
||||
if (data.pageId) {
|
||||
state.currentNodeData = { id: data.pageId }
|
||||
}
|
||||
},
|
||||
subscriber: 'routeBar'
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (subscriber) {
|
||||
unsubscribe(subscriber)
|
||||
}
|
||||
})
|
||||
|
||||
const refreshPageList = async (appId) => {
|
||||
const pages = await getPageList(appId)
|
||||
|
||||
|
|
@ -130,58 +149,13 @@ export default {
|
|||
return pageList
|
||||
}
|
||||
|
||||
const clearCurrentState = () => {
|
||||
pageState.currentVm = null
|
||||
pageState.hoverVm = null
|
||||
pageState.properties = {}
|
||||
pageState.pageSchema = null
|
||||
}
|
||||
|
||||
const updateUrlPageId = (id) => {
|
||||
const url = new URL(window.location)
|
||||
|
||||
url.searchParams.delete('blockid')
|
||||
url.searchParams.set('pageid', id)
|
||||
window.history.pushState({}, '', url)
|
||||
postLocationHistoryChanged({ pageId: id })
|
||||
}
|
||||
|
||||
const getPageDetail = (pageId) => {
|
||||
// pageId !== 0 防止 pageId 为 0 的时候判断不出来
|
||||
if (pageId !== 0 && !pageId) {
|
||||
updateUrlPageId('')
|
||||
initData({ componentName: COMPONENT_NAME.Page }, {})
|
||||
useLayout().layoutState.pageStatus = {
|
||||
state: 'empty',
|
||||
data: {}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fetchPageDetail(pageId).then((data) => {
|
||||
updateUrlPageId(pageId)
|
||||
closePageSettingPanel()
|
||||
closeFolderSettingPanel()
|
||||
useLayout().closePlugin()
|
||||
useLayout().layoutState.pageStatus = getCanvasStatus(data.occupier)
|
||||
initData(data['page_content'], data)
|
||||
})
|
||||
}
|
||||
|
||||
const switchPage = (data) => {
|
||||
pageState.hoverVm = null
|
||||
state.currentNodeData = data
|
||||
|
||||
let pageName = ''
|
||||
if (data.isPage) {
|
||||
pageName = data?.name || ''
|
||||
}
|
||||
setBreadcrumbPage([pageName])
|
||||
|
||||
// 切换页面时清空 选中节点信息状态
|
||||
clearCurrentState()
|
||||
getPageDetail(data.id)
|
||||
switchPageById(data.id).then(() => {
|
||||
closePageSettingPanel()
|
||||
closeFolderSettingPanel()
|
||||
})
|
||||
}
|
||||
|
||||
const nodeClick = (e, pageData) => {
|
||||
|
|
|
|||
|
|
@ -13,10 +13,19 @@
|
|||
import { reactive, ref } from 'vue'
|
||||
import { extend, isEqual } from '@opentiny/vue-renderless/common/object'
|
||||
import { constants } from '@opentiny/tiny-engine-utils'
|
||||
import { getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
|
||||
import { getCanvasStatus } from '@opentiny/tiny-engine-common/js/canvas'
|
||||
import {
|
||||
useCanvas,
|
||||
useLayout,
|
||||
useBreadcrumb,
|
||||
useModal,
|
||||
useNotify,
|
||||
getMetaApi,
|
||||
META_SERVICE
|
||||
} from '@opentiny/tiny-engine-meta-register'
|
||||
import http from '../http'
|
||||
|
||||
const { ELEMENT_TAG } = constants
|
||||
const { ELEMENT_TAG, COMPONENT_NAME } = constants
|
||||
|
||||
import { useMessage } from '@opentiny/tiny-engine-meta-register'
|
||||
const { publish } = useMessage()
|
||||
|
|
@ -178,7 +187,7 @@ const generateTree = (data) => {
|
|||
}
|
||||
|
||||
const getPageList = async (appId) => {
|
||||
const pagesData = await http.fetchPageList(appId)
|
||||
const pagesData = await http.fetchPageList(appId || getMetaApi(META_SERVICE.GlobalService).getBaseInfo().id)
|
||||
|
||||
const firstGroupData = { groupName: '静态页面', groupId: STATIC_PAGE_GROUP_ID, data: [] }
|
||||
const secondGroupData = { groupName: '公共页面', groupId: COMMON_PAGE_GROUP_ID, data: [] }
|
||||
|
|
@ -213,16 +222,15 @@ const getPageList = async (appId) => {
|
|||
|
||||
/**
|
||||
* @param {string | number} id
|
||||
* @param {(string | number)[]} ancestors
|
||||
* @returns {(string | number)[]}
|
||||
* @returns {any[]}
|
||||
*/
|
||||
const getAncestorsRecursively = (id) => {
|
||||
const pageNode = pageSettingState.treeDataMapping[id]
|
||||
|
||||
if (pageNode.id === pageSettingState.ROOT_ID) {
|
||||
if (id === pageSettingState.ROOT_ID) {
|
||||
return []
|
||||
}
|
||||
|
||||
const pageNode = pageSettingState.treeDataMapping[id]
|
||||
|
||||
return [pageNode].concat(getAncestorsRecursively(pageNode.parentId))
|
||||
}
|
||||
|
||||
|
|
@ -233,24 +241,109 @@ const getAncestorsRecursively = (id) => {
|
|||
*/
|
||||
const getAncestors = async (id, withFolders) => {
|
||||
if (pageSettingState.pages.length === 0) {
|
||||
const appId = getMetaApi(META_SERVICE.GlobalService).getBaseInfo().id
|
||||
await getPageList(appId)
|
||||
await getPageList()
|
||||
}
|
||||
|
||||
if (!pageSettingState.treeDataMapping[id]) {
|
||||
return null
|
||||
}
|
||||
|
||||
const ancestorsWithSelf = getAncestorsRecursively(id)
|
||||
const ancestors = ancestorsWithSelf.slice(1).reverse()
|
||||
|
||||
if (withFolders) {
|
||||
return ancestors.map((item) => item.id)
|
||||
const predicate = withFolders ? () => true : (item) => item.isPage
|
||||
|
||||
return ancestors.filter(predicate).map((item) => item.id)
|
||||
}
|
||||
|
||||
const clearCurrentState = () => {
|
||||
const { pageState } = useCanvas()
|
||||
|
||||
pageState.currentVm = null
|
||||
pageState.hoverVm = null
|
||||
pageState.properties = {}
|
||||
pageState.pageSchema = null
|
||||
}
|
||||
|
||||
const updateUrlPageId = (id) => {
|
||||
const url = new URL(window.location)
|
||||
|
||||
url.searchParams.delete('blockid')
|
||||
url.searchParams.set('pageid', id)
|
||||
window.history.pushState({}, '', url)
|
||||
postLocationHistoryChanged({ pageId: id })
|
||||
}
|
||||
|
||||
const switchPage = (pageId) => {
|
||||
// 切换页面时清空 选中节点信息状态
|
||||
clearCurrentState()
|
||||
|
||||
// pageId !== 0 防止 pageId 为 0 的时候判断不出来
|
||||
if (pageId !== 0 && !pageId) {
|
||||
updateUrlPageId('')
|
||||
useCanvas().initData({ componentName: COMPONENT_NAME.Page }, {})
|
||||
useLayout().layoutState.pageStatus = {
|
||||
state: 'empty',
|
||||
data: {}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return ancestors.filter((item) => item.isPage).map((item) => item.id)
|
||||
return http
|
||||
.fetchPageDetail(pageId)
|
||||
.then((data) => {
|
||||
if (data.isPage) {
|
||||
// 应该改成让 Breadcrumb 插件去监听变化
|
||||
useBreadcrumb().setBreadcrumbPage([data.name])
|
||||
}
|
||||
|
||||
updateUrlPageId(pageId)
|
||||
useLayout().closePlugin()
|
||||
useLayout().layoutState.pageStatus = getCanvasStatus(data.occupier)
|
||||
useCanvas().initData(data['page_content'], data)
|
||||
})
|
||||
.catch(() => {
|
||||
useNotify({
|
||||
type: 'error',
|
||||
message: '切换页面失败,目标页面不存在'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const switchPageWithConfirm = (pageId) => {
|
||||
const checkPageSaved = () => {
|
||||
const { isSaved, isBlock } = useCanvas()
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (isSaved()) {
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
|
||||
useModal().confirm({
|
||||
title: '提示',
|
||||
message: `${isBlock() ? '区块' : '页面'}尚未保存,是否要继续切换?`,
|
||||
exec: () => {
|
||||
resolve(true)
|
||||
},
|
||||
cancel: () => {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
checkPageSaved().then((proceed) => {
|
||||
if (proceed) {
|
||||
switchPage(pageId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getFamily = async (id) => {
|
||||
if (pageSettingState.pages.length === 0) {
|
||||
const appId = getMetaApi(META_SERVICE.GlobalService).getBaseInfo().id
|
||||
await getPageList(appId)
|
||||
await getPageList()
|
||||
}
|
||||
|
||||
return getAncestorsRecursively(id)
|
||||
|
|
@ -273,6 +366,8 @@ export default () => {
|
|||
isChangePageData,
|
||||
getPageList,
|
||||
getAncestors,
|
||||
switchPage,
|
||||
switchPageWithConfirm,
|
||||
getFamily,
|
||||
STATIC_PAGE_GROUP_ID,
|
||||
COMMON_PAGE_GROUP_ID
|
||||
|
|
|
|||
Loading…
Reference in New Issue