<template>
<div v-show="menuState.show" ref="menuDom" class="context-menu" :style="menuState.position">
<ul class="menu-item">
<li
v-for="(item, index) in filteredMenus"
:key="index"
:class="{
'li-item': item.items,
'li-item-disabled': actionDisabled(item)
}"
@click="doOperation(item)"
@mouseover="onShowChildrenMenu(item)"
>
<div>
<span>{{ item.name }}</span>
<span v-if="item.items"><icon-right></icon-right></span>
</div>
<ul v-if="item.items && current === item" class="sub-menu menu-item" :style="subMenuStyles">
<template v-for="(subItem, subIndex) in item.items" :key="subIndex">
<li
:class="[{ 'menu-item-disabled': subItem.check && !subItem.check?.() }]"
@click.stop="doOperation(subItem)"
>
{{ subItem.name }}
</li>
</template>
</ul>
</li>
</ul>
<component :is="SaveNewBlock" :boxVisibility="boxVisibility" fromCanvas @close="close"></component>
</div>
</template>
<script lang="jsx">
import { ref, reactive, nextTick, computed } from 'vue'
import { canvasState, getConfigure, getController, getCurrent, copyNode, removeNodeById } from '../container'
import { useLayout, useModal, useCanvas, usePage, getMergeMeta } from '@opentiny/tiny-engine-meta-register'
import { iconRight } from '@opentiny/vue-icon'
import { useMultiSelect } from '../composables/useMultiSelect'
const menuState = reactive({
position: null,
show: false,
menus: []
})
const current = ref(null)
const menuDom = ref(null)
const subMenuStyles = ref(null)
const SUB_MENU_WIDTH = 137
export const closeMenu = () => {
menuState.show = false
current.value = null
}
export const openMenu = (event) => {
menuState.position = {
left: event.clientX + 2 + 'px',
top: event.clientY + 'px'
}
menuState.show = sessionStorage.getItem('pageInfo') ? true : false
nextTick(() => {
if (menuDom.value) {
const { right, bottom, width, height } = menuDom.value.getBoundingClientRect()
const canvasRect = canvasState.iframe.getBoundingClientRect()
if (bottom > canvasRect.bottom) {
menuState.position.top = `${parseInt(menuState.position.top) - height}px`
}
if (right > canvasRect.right) {
menuState.position.left = `${parseInt(menuState.position.left) - width - 2}px`
}
if (right + SUB_MENU_WIDTH < canvasRect.right) {
subMenuStyles.value = { right: `-${SUB_MENU_WIDTH}px`, width: `${SUB_MENU_WIDTH}px` }
} else {
subMenuStyles.value = { left: `-${SUB_MENU_WIDTH}px`, width: `${SUB_MENU_WIDTH}px` }
}
}
})
}
export default {
components: {
IconRight: iconRight()
},
setup(props, { emit }) {
const { multiSelectedStates, areSiblingNodes, batchAddParent, groupAddParent } = useMultiSelect()
const menus = ref([
{ name: '修改属性', code: 'config' },
{
name: '插入',
items: [
{ name: '向前', code: 'insert', value: 'top' },
{
name: '中间',
code: 'insert',
value: 'in',
check() {
const { componentName } = getCurrent()?.schema || {}
return getConfigure(componentName)?.isContainer
}
},
{ name: '向后', code: 'insert', value: 'bottom' }
]
},
{
name: '添加父级',
items: [
{ name: '文字提示', code: 'wrap', value: 'TinyTooltip' },
{ name: '弹出框', code: 'wrap', value: 'TinyPopover' },
{ name: '容器', code: 'insert', value: 'out' }
],
code: 'addParent'
},
{ name: '删除', code: 'del' },
{ name: '复制', code: 'copy' },
{ name: '绑定事件', code: 'bindEvent' }
])
const multiSelectMenus = ref([
{ name: '删除', code: 'multiDel' },
{ name: '复制', code: 'multiCopy' },
{
name: '添加父级',
items: [
{
name: '容器(批量)',
code: 'batchWrap',
value: 'div'
},
{
name: '容器(公共父级)',
code: 'groupWrap',
value: 'div',
check: () => areSiblingNodes()
},
{
name: '弹出框(公共父级)',
code: 'groupWrap',
value: 'TinyPopover',
check: () => areSiblingNodes()
}
],
code: 'multiAddParent'
}
])
const { SaveNewBlock } = getMergeMeta('engine.plugins.blockmanage')?.components || {}
if (SaveNewBlock) {
menus.value.push({ name: '新建区块', code: 'createBlock' })
multiSelectMenus.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 isMultiSelect = computed(() => multiSelectedStates.value.length > 1)
const filteredMenus = computed(() => {
if (isMultiSelect.value) {
return multiSelectMenus.value.filter((item) => {
if (typeof item.show === 'function') {
return item.show()
}
return true
})
}
return menus.value.filter((item) => {
if (typeof item.show === 'function') {
return item.show()
}
return true
})
})
const boxVisibility = ref(false)
const { PLUGIN_NAME, activeSetting } = useLayout()
const operations = {
del() {
removeNodeById(getCurrent().schema?.id)
},
copy() {
copyNode(getCurrent().schema?.id)
},
multiDel() {
const ids = multiSelectedStates.value.map((state) => state.id)
ids.forEach((id) => removeNodeById(id))
},
multiCopy() {
const ids = multiSelectedStates.value.map((state) => state.id)
ids.forEach((id) => copyNode(id))
useCanvas().canvasApi.value.updateRect?.()
},
config() {
activeSetting(PLUGIN_NAME.Props)
},
bindEvent() {
activeSetting(PLUGIN_NAME.Event)
},
insert({ value }) {
emit('insert', value)
},
wrap({ value, name }) {
const componentName = value || name
const { schema, parent } = getCurrent()
if (!schema || !parent) {
return
}
const index = parent.children.findIndex(({ id }) => schema.id === id)
let wrapSchema = {
componentName,
id: null,
props: {
content: '提示信息'
},
children: [schema]
}
if (value === 'TinyPopover') {
wrapSchema = {
componentName,
props: {
width: 200,
title: '弹框标题',
trigger: 'manual',
modelValue: true
},
children: [
{
componentName: 'Template',
props: {
slot: 'reference'
},
children: [schema]
},
{
componentName: 'Template',
props: {
slot: 'default'
},
children: [
{
componentName: 'div',
props: {
placeholder: '提示内容'
}
}
]
}
]
}
}
parent.children.splice(index, 1, wrapSchema)
getController().addHistory()
},
batchWrap({ value }) {
batchAddParent(value)
},
groupWrap({ value }) {
groupAddParent(value)
},
createBlock() {
if (useCanvas().isSaved()) {
boxVisibility.value = true
} else {
useModal().message({
message: '请先保存当前页面',
status: 'error'
})
}
},
route() {
const targetPageId = getCurrent().schema.props.to.name
usePage().switchPageWithConfirm(targetPageId, true)
}
}
const actionDisabled = (actionItem) => {
if (typeof actionItem.check === 'function' && !actionItem.check()) {
return true
}
if (isMultiSelect.value) {
const multiSelectActions = ['multiDel', 'multiCopy', 'multiAddParent']
return multiSelectActions.includes(actionItem.code) && multiSelectedStates.value.length === 0
} else {
const actions = ['del', 'copy', 'addParent']
return actions.includes(actionItem.code) && !getCurrent().schema?.id
}
}
const onShowChildrenMenu = (menuItem) => {
current.value = !actionDisabled(menuItem) ? menuItem : null
}
const close = () => {
boxVisibility.value = false
}
const doOperation = (item) => {
if (actionDisabled(item)) {
return
}
if (item?.code) {
operations[item.code]?.(item)
closeMenu()
}
}
return {
SaveNewBlock,
menuState,
filteredMenus,
doOperation,
boxVisibility,
close,
current,
menuDom,
subMenuStyles,
actionDisabled,
onShowChildrenMenu
}
}
}
</script>
<style lang="less" scoped>
.context-menu {
position: absolute;
z-index: 10;
}
.menu-item {
width: 140px;
line-height: 20px;
border-radius: 6px;
padding: 8px 0;
background-color: var(--te-canvas-container-bg-color);
box-shadow: 0 1px 15px 0 rgb(0 0 0 / 20%);
display: flex;
flex-direction: column;
.li-item {
border-bottom: 1px solid var(--te-canvas-container-border-color);
}
.li-item-disabled {
cursor: not-allowed;
color: var(--te-canvas-container-text-color-disabled);
svg {
fill: var(--te-canvas-container-text-color-disabled);
}
}
li {
& > div {
display: flex;
width: 100%;
justify-content: space-between;
}
font-size: 12px;
color: var(--te-canvas-container-text-color-primary);
svg {
fill: var(--te-canvas-container-text-color-primary);
}
padding: 6px 15px;
&:not(.menu-item-disabled):hover {
background: var(--te-canvas-container-bg-color-hover);
}
position: relative;
&.menu-item-disabled {
cursor: not-allowed;
color: var(--te-canvas-container-text-color-disabled);
}
}
&.sub-menu {
position: absolute;
top: -2px;
}
}
</style>