<template>
|
<div
|
class="shape"
|
:class="{
|
'shape-group-area': isGroupArea,
|
'freeze-component': freezeFlag,
|
'freeze-component-fullscreen': freezeFlag && fullscreenFlag
|
}"
|
ref="shapeInnerRef"
|
:id="domId"
|
v-loading="downLoading"
|
element-loading-text="导出中..."
|
element-loading-background="rgba(255, 255, 255, 1)"
|
@dblclick="handleDbClick"
|
>
|
<div
|
:title="t('visualization.sync_pc_design')"
|
v-if="showCheck"
|
class="refresh-from-pc"
|
@click="updateFromMobile($event, 'syncPcDesign')"
|
>
|
<el-icon>
|
<Icon name="icon_replace_outlined"><replaceOutlined class="svg-icon" /></Icon>
|
</el-icon>
|
</div>
|
<div
|
v-if="showCheck"
|
class="del-from-mobile"
|
@click="updateFromMobile($event, 'delFromMobile')"
|
>
|
<el-icon>
|
<Icon name="mobile-checkbox"><mobileCheckbox class="svg-icon" /></Icon>
|
</el-icon>
|
</div>
|
<div v-if="showHiddenIcon" class="del-from-mobile" @mousedown.stop="hiddenComponent">
|
<el-tooltip :content="$t('visualization.hidden')" placement="bottom">
|
<el-icon @click.stop>
|
<Icon @click.stop name="dvHidden"><dvHidden class="svg-icon" /></Icon>
|
</el-icon>
|
</el-tooltip>
|
</div>
|
<div
|
class="shape-outer"
|
v-show="contentDisplay"
|
:class="{
|
active,
|
'shape-lock': shapeLock,
|
'shape-edit': isEditMode && !boardMoveActive,
|
'linkage-setting': linkageActive,
|
'drag-on-tab-collision': dragCollision,
|
'shape-selected': curBatchOptComponents?.includes(element.id)
|
}"
|
>
|
<component-edit-bar
|
ref="componentEditBarRef"
|
class="edit-bar"
|
:class="{ 'edit-bar-active': editBarShowFlag }"
|
:index="index"
|
:element="element"
|
:show-position="showPosition"
|
:canvas-id="canvasId"
|
@componentImageDownload="htmlToImage"
|
@userViewEnlargeOpen="userViewEnlargeOpen"
|
@datasetParamsInit="datasetParamsInit"
|
@linkJumpSetOpen="linkJumpSetOpen"
|
@linkageSetOpen="linkageSetOpen"
|
></component-edit-bar>
|
<div
|
class="shape-inner"
|
ref="componentInnerRef"
|
:id="viewDemoInnerId"
|
:style="componentBackgroundStyle"
|
@click="selectCurComponent"
|
@mousedown="handleInnerMouseDownOnShape"
|
>
|
<Icon v-if="shapeLock" name="dv-lock"><dvLock class="svg-icon iconfont icon-suo" /></Icon>
|
<div class="component-slot" :style="slotStyle">
|
<slot></slot>
|
</div>
|
<!--边框背景-->
|
<Board
|
v-if="svgInnerEnable"
|
:style="{ color: element.commonBackground.innerImageColor, pointerEvents: 'none' }"
|
:name="commonBackgroundSvgInner"
|
></Board>
|
</div>
|
<div
|
v-for="item in isActive() ? getPointList() : []"
|
:key="item"
|
class="shape-point"
|
:style="getPointStyle(item)"
|
@mousedown="handleMouseDownOnPoint(item, $event)"
|
></div>
|
<div
|
class="shape-shadow"
|
v-show="batchOptFlag && element.component !== 'DeTabs'"
|
@mousedown="batchSelected"
|
></div>
|
<template v-if="boardMoveActive">
|
<div
|
v-show="!element.editing"
|
class="de-drag-area de-drag-top"
|
@mousedown="handleBoardMouseDownOnShape"
|
/>
|
<div
|
v-show="!element.editing && element.component !== 'DeTabs'"
|
class="de-drag-area de-drag-right"
|
@mousedown="handleBoardMouseDownOnShape"
|
/>
|
<div
|
v-show="!element.editing && element.component !== 'DeTabs'"
|
class="de-drag-area de-drag-bottom"
|
@mousedown="handleBoardMouseDownOnShape"
|
/>
|
<div
|
v-show="!element.editing && element.component !== 'DeTabs'"
|
class="de-drag-area de-drag-left"
|
@mousedown="handleBoardMouseDownOnShape"
|
/>
|
</template>
|
</div>
|
<compose-show :element="element"></compose-show>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
import mobileCheckbox from '@/assets/svg/mobile-checkbox.svg'
|
import replaceOutlined from '@/assets/svg/icon_replace_outlined.svg'
|
import dvLock from '@/assets/svg/dv-lock.svg'
|
import eventBus from '@/utils/eventBus'
|
import calculateComponentPositionAndSize, {
|
calculateRadioComponentPositionAndSize
|
} from '@/utils/calculateComponentPositionAndSize'
|
import { mod360 } from '@/utils/translate'
|
import { deepCopy } from '@/utils/utils'
|
import { computed, nextTick, onMounted, ref, toRefs, reactive } from 'vue'
|
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
|
import { snapshotStoreWithOut } from '@/store/modules/data-visualization/snapshot'
|
import { contextmenuStoreWithOut } from '@/store/modules/data-visualization/contextmenu'
|
import { composeStoreWithOut } from '@/store/modules/data-visualization/compose'
|
import { storeToRefs } from 'pinia'
|
import { downloadCanvas2, imgUrlTrans } from '@/utils/imgUtils'
|
import Icon from '@/components/icon-custom/src/Icon.vue'
|
import ComponentEditBar from '@/components/visualization/ComponentEditBar.vue'
|
import { useEmitt } from '@/hooks/web/useEmitt'
|
import ComposeShow from '@/components/data-visualization/canvas/ComposeShow.vue'
|
import dvHidden from '@/assets/svg/dv-hidden.svg'
|
import { groupSizeStyleAdaptor, groupStyleRevert, tabInnerStyleRevert } from '@/utils/style'
|
import {
|
checkJoinTab,
|
isDashboard,
|
isGroupCanvas,
|
isMainCanvas,
|
isTabCanvas,
|
itemCanvasPathCheck
|
} from '@/utils/canvasUtils'
|
import Board from '@/components/de-board/Board.vue'
|
import { activeWatermarkCheckUser, removeActiveWatermark } from '@/components/watermark/watermark'
|
import { useI18n } from '@/hooks/web/useI18n'
|
import { CommonBackground } from '@/components/visualization/component-background/Types'
|
import { ShorthandMode } from '@/Types'
|
const { t } = useI18n()
|
const dvMainStore = dvMainStoreWithOut()
|
const snapshotStore = snapshotStoreWithOut()
|
const contextmenuStore = contextmenuStoreWithOut()
|
const composeStore = composeStoreWithOut()
|
const parentNode = ref(null)
|
const shapeInnerRef = ref(null)
|
const componentInnerRef = ref(null)
|
const componentEditBarRef = ref(null)
|
const downLoading = ref(false)
|
const viewDemoInnerId = computed(() => 'enlarge-inner-shape-' + element.value.id)
|
|
const {
|
curComponent,
|
dvInfo,
|
editMode,
|
batchOptStatus,
|
curBatchOptComponents,
|
linkageSettingStatus,
|
curLinkageView,
|
tabCollisionActiveId,
|
tabMoveInActiveId,
|
tabMoveOutComponentId,
|
mobileInPc,
|
mainScrollTop,
|
hiddenListStatus,
|
fullscreenFlag
|
} = storeToRefs(dvMainStore)
|
const { editorMap, areaData, isCtrlOrCmdDown } = storeToRefs(composeStore)
|
const emit = defineEmits([
|
'userViewEnlargeOpen',
|
'datasetParamsInit',
|
'onStartResize',
|
'onStartMove',
|
'onDragging',
|
'onResizing',
|
'onMouseUp',
|
'linkJumpSetOpen',
|
'linkageSetOpen'
|
])
|
|
const isEditMode = computed(() => editMode.value === 'edit')
|
const state = reactive({
|
seriesIdMap: {
|
id: ''
|
},
|
// 禁止移入Tab中的组件
|
ignoreTabMoveComponent: ['de-button', 'de-reset-button', 'DeTabs', 'GroupArea'],
|
// 当画布在tab中是 宽度左右拓展的余量
|
parentWidthTabOffset: 40,
|
canvasChangeTips: 'none',
|
tabMoveInYOffset: 70,
|
tabMoveInXOffset: 40,
|
collisionGap: 10 // 碰撞深度有效区域,
|
})
|
const hiddenComponent = event => {
|
event.preventDefault()
|
event.stopPropagation()
|
if (element.value) {
|
element.value.dashboardHidden = true
|
eventBus.emit('removeMatrixItemPosition-' + canvasId.value, element.value)
|
snapshotStore.recordSnapshotCache('hide')
|
dvMainStore.setLastHiddenComponent(element.value.id)
|
}
|
}
|
const showHiddenIcon = computed(() => hiddenListStatus.value && isMainCanvas(canvasId.value))
|
const contentDisplay = ref(true)
|
const shapeLock = computed(() => {
|
return element.value['isLock'] && isEditMode.value
|
})
|
|
const showPosition = computed(() => {
|
let position
|
// 数据大屏批量操作合并为分组
|
if (batchOptFlag.value) {
|
position = 'batchOpt'
|
} else if (isEditMode.value) {
|
position = dvInfo.value.type === 'dashboard' ? 'canvas' : 'canvasDataV'
|
} else {
|
position = 'preview'
|
}
|
return position
|
})
|
|
const props = defineProps({
|
element: {
|
required: true,
|
type: Object,
|
default() {
|
return {
|
component: null,
|
propValue: null,
|
request: null,
|
linkage: null,
|
type: null,
|
events: null,
|
style: null,
|
id: null
|
}
|
}
|
},
|
defaultStyle: {
|
required: true,
|
type: Object,
|
default() {
|
return {}
|
}
|
},
|
baseCellInfo: {
|
required: true,
|
type: Object,
|
default() {
|
return {
|
baseWidth: 0,
|
baseHeight: 0,
|
curGap: 0
|
}
|
}
|
},
|
index: {
|
required: true,
|
type: [Number, String],
|
default: 0
|
},
|
// tab 移入检测
|
isTabMoveCheck: {
|
type: Boolean,
|
default: true
|
},
|
canvasId: {
|
type: String,
|
default: 'canvas-main'
|
},
|
scale: {
|
type: Number,
|
required: false,
|
default: 1
|
},
|
canvasActive: {
|
type: Boolean,
|
required: false,
|
default: true
|
}
|
})
|
|
const {
|
element,
|
defaultStyle,
|
baseCellInfo,
|
index,
|
isTabMoveCheck,
|
canvasId,
|
scale,
|
canvasActive
|
} = toRefs(props)
|
|
const pTabGroupFlag = itemCanvasPathCheck(element.value, 'pTabGroup')
|
const pJoinTab = checkJoinTab(element.value)
|
const domId = ref('shape-id-' + element.value.id)
|
const pointList = ['lt', 't', 'rt', 'r', 'rb', 'b', 'lb', 'l']
|
const pointCorner = ['lt', 'rt', 'rb', 'lb']
|
const pointList2 = ['r', 'l']
|
const initialAngle = {
|
// 每个点对应的初始角度
|
lt: 0,
|
t: 45,
|
rt: 90,
|
r: 135,
|
rb: 180,
|
b: 225,
|
lb: 270,
|
l: 315
|
}
|
const cursors = ref({})
|
|
const freezeFlag = computed(() => {
|
return (
|
isMainCanvas(canvasId.value) &&
|
element.value.freeze &&
|
!mobileInPc.value &&
|
mainScrollTop.value - defaultStyle.value.top > 0
|
)
|
})
|
|
const showCheck = computed(() => {
|
return mobileInPc.value && element.value.canvasId === 'canvas-main'
|
})
|
|
const updateFromMobile = (e, type) => {
|
if (type === 'syncPcDesign') {
|
e.preventDefault()
|
e.stopPropagation()
|
}
|
useEmitt().emitter.emit('onMobileStatusChange', {
|
type: type,
|
value: element.value.id
|
})
|
}
|
|
const angleToCursor = [
|
// 每个范围的角度对应的光标
|
{ start: 338, end: 23, cursor: 'nw' },
|
{ start: 23, end: 68, cursor: 'n' },
|
{ start: 68, end: 113, cursor: 'ne' },
|
{ start: 113, end: 158, cursor: 'e' },
|
{ start: 158, end: 203, cursor: 'se' },
|
{ start: 203, end: 248, cursor: 's' },
|
{ start: 248, end: 293, cursor: 'sw' },
|
{ start: 293, end: 338, cursor: 'w' }
|
]
|
|
const isGroupArea = computed(() => {
|
return curComponent.value?.component === 'GroupArea'
|
})
|
|
const active = computed(() => {
|
return curComponent.value?.id === element.value.id
|
})
|
|
const boardMoveActive = computed(() => {
|
const CHARTS = [
|
'flow-map',
|
'map',
|
'bubble-map',
|
'table-info',
|
'table-normal',
|
'table-pivot',
|
'symbolic-map',
|
'heat-map',
|
't-heatmap',
|
'circle-packing'
|
]
|
return element.value.isPlugin || CHARTS.includes(element.value.innerType)
|
})
|
|
const dashboardActive = computed(() => {
|
return dvInfo.value.type === 'dashboard'
|
})
|
|
const getPointList = () => {
|
return element.value.component === 'line-shape' ? pointList2 : pointList
|
}
|
|
const isActive = () => {
|
return active.value && !element.value['isLock'] && isEditMode.value && !freezeFlag.value
|
}
|
|
const userViewEnlargeOpen = opt => {
|
emit('userViewEnlargeOpen', opt)
|
}
|
|
const datasetParamsInit = opt => {
|
emit('datasetParamsInit', opt)
|
}
|
|
const getPointStyle = point => {
|
let { width, height } = defaultStyle.value
|
const { sizeX, sizeY } = element.value
|
if (dashboardActive.value && !element.value['isPlayer']) {
|
width = sizeX * baseCellInfo.value.baseWidth - 2 * baseCellInfo.value.curGap
|
height = sizeY * baseCellInfo.value.baseHeight - 2 * baseCellInfo.value.curGap
|
} else {
|
width = width - 2 * baseCellInfo.value.curGap
|
height = height - 2 * baseCellInfo.value.curGap
|
}
|
const hasT = /t/.test(point)
|
const hasB = /b/.test(point)
|
const hasL = /l/.test(point)
|
const hasR = /r/.test(point)
|
let newLeft = 0
|
let newTop = 0
|
|
// 四个角的点
|
if (point.length === 2) {
|
newLeft = hasL ? 0 : width
|
newTop = hasT ? 0 : height
|
} else {
|
// 上下两点的点,宽度居中
|
if (hasT || hasB) {
|
newLeft = width / 2
|
newTop = hasT ? 0 : height
|
}
|
|
// 左右两边的点,高度居中
|
if (hasL || hasR) {
|
newLeft = hasL ? 0 : width
|
newTop = Math.floor(height / 2)
|
}
|
}
|
|
const style = {
|
marginLeft: '-4px',
|
marginTop: '-4px',
|
left: `${newLeft}px`,
|
top: `${newTop}px`,
|
cursor: cursors.value[point]
|
}
|
|
return style
|
}
|
|
const getCursor = () => {
|
const rotate = mod360(curComponent.value.style.rotate) // 取余 360
|
const result = {}
|
let lastMatchIndex = -1 // 从上一个命中的角度的索引开始匹配下一个,降低时间复杂度
|
|
pointList.forEach(point => {
|
const angle = mod360(initialAngle[point] + rotate)
|
const len = angleToCursor.length
|
// eslint-disable-next-line no-constant-condition
|
while (true) {
|
lastMatchIndex = (lastMatchIndex + 1) % len
|
const angleLimit = angleToCursor[lastMatchIndex]
|
if (angle < 23 || angle >= 338) {
|
result[point] = 'nw-resize'
|
|
return
|
}
|
|
if (angleLimit.start <= angle && angle < angleLimit.end) {
|
result[point] = angleLimit.cursor + '-resize'
|
|
return
|
}
|
}
|
})
|
|
return result
|
}
|
|
const handleBoardMouseDownOnShape = e => {
|
if (!canvasActive.value) {
|
return
|
}
|
dvMainStore.setCurComponent({ component: element.value, index: index.value })
|
handleMouseDownOnShape(e)
|
}
|
|
const areaDataPush = component => {
|
if (
|
component &&
|
!component.isLock &&
|
component.isShow &&
|
component.canvasId === 'canvas-main' &&
|
!['GroupArea'].includes(component.component)
|
) {
|
areaData.value.components.push(component)
|
}
|
}
|
const handleDbClick = () => {
|
if (element.value.canvasId !== 'canvas-main') {
|
dvMainStore.setCurComponent({ component: element.value, index: index.value })
|
}
|
}
|
|
const handleInnerMouseDownOnShape = e => {
|
if (!canvasActive.value) {
|
return
|
}
|
batchSelected(e)
|
// ctrl or command 按下时 鼠标点击为选择需要组合的组件(取消需要组合的组件在ComposeShow组件中)
|
if (isCtrlOrCmdDown.value && !areaData.value.components.includes(element)) {
|
areaDataPush(element.value)
|
if (curComponent.value && curComponent.value.id !== element.value.id) {
|
areaDataPush(curComponent.value)
|
}
|
e.stopPropagation()
|
return
|
}
|
dvMainStore.setCurComponent({ component: element.value, index: index.value })
|
// 边界区域拖拽 返回
|
if (boardMoveActive.value) {
|
dvMainStore.setInEditorStatus(true)
|
dvMainStore.setClickComponentStatus(true)
|
e.stopPropagation()
|
return
|
}
|
handleMouseDownOnShape(e)
|
}
|
|
const handleMouseDownOnShape = e => {
|
if (element.value['editing']) {
|
// e.preventDefault()
|
e.stopPropagation()
|
return
|
}
|
dashboardActive.value && emit('onStartMove', e)
|
// 将当前点击组件的事件传播出去
|
nextTick(() => eventBus.emit('componentClick'))
|
dvMainStore.setInEditorStatus(true)
|
dvMainStore.setClickComponentStatus(true)
|
// if (isPreventDrop(element.value.component)) {
|
// e.preventDefault()
|
// }
|
|
e.stopPropagation()
|
// 锁定 非编辑状态 冻结状态 不进行移动
|
if (element.value['isLock'] || !isEditMode.value || freezeFlag.value) return
|
|
cursors.value = getCursor() // 根据旋转角度获取光标位置
|
|
const pos = { ...defaultStyle.value }
|
const startY = e.clientY
|
const startX = e.clientX
|
|
const offsetY = e.offsetY
|
const offsetX = e.offsetX
|
|
// 如果直接修改属性,值的类型会变为字符串,所以要转为数值型
|
const startTop = Number(pos['top'])
|
const startLeft = Number(pos['left'])
|
|
// 如果元素没有移动,则不保存快照
|
let hasMove = false
|
let isFirst = true
|
|
// 画布宽高
|
const canvasWidth = parentNode.value.offsetWidth
|
//当前组件宽高 定位
|
const componentWidth = shapeInnerRef.value.offsetWidth
|
const componentHeight = shapeInnerRef.value.offsetHeight
|
let outerTabDom = isTabCanvas(canvasId.value)
|
? document.getElementById('shape-id-' + canvasId.value.split('--')[0])
|
: null
|
const curDom = document.getElementById(domId.value)
|
|
// 添加节流变量
|
let rafId = null
|
let lastTime = 0
|
const throttleInterval = 25 // 约30FPS,可以根据需要调整
|
|
const move = moveEvent => {
|
const now = Date.now()
|
|
// 节流处理
|
if (now - lastTime < throttleInterval && rafId !== null) {
|
return
|
}
|
|
// 使用 requestAnimationFrame 来优化性能
|
if (rafId) {
|
cancelAnimationFrame(rafId)
|
}
|
|
rafId = requestAnimationFrame(() => {
|
hasMove = true
|
const curX = moveEvent.clientX
|
const curY = moveEvent.clientY
|
const top = curY - startY + startTop
|
const left = curX - startX + startLeft
|
pos['top'] = top
|
pos['left'] = left
|
// 非主画布非分组画布的情况 需要检测是否从Tab中移除组件(向左移除30px 或者向右移除30px 向左移除30px)
|
// 因为仪表板中组件向下移动可能只是为了挤占空间 不一定是为了移出 这里无法判断明确意图 暂时支不支持向下移出
|
// 大屏和仪表板暂时做位置算法区分 仪表板暂时使用curX 因为缩放的影响 大屏使用 tab位置 + 组件位置(相对内部画布)+初始触发点
|
// 如果组件在tab中且tab在Group中 不允许移入移出 pTabGroupFlag = true
|
if (
|
!pTabGroupFlag &&
|
pJoinTab &&
|
!isMainCanvas(canvasId.value) &&
|
!isGroupCanvas(canvasId.value) &&
|
!isGroupArea.value &&
|
(top < -30 || left < -30 || left + componentWidth - canvasWidth > 30)
|
) {
|
contentDisplay.value = false
|
dvMainStore.setMousePointShadowMap({
|
mouseX:
|
!isDashboard() && outerTabDom
|
? outerTabDom.offsetLeft + curDom.offsetLeft + offsetX
|
: curX,
|
mouseY:
|
!isDashboard() && outerTabDom
|
? outerTabDom.offsetTop + curDom.offsetTop + offsetY + 100
|
: curY + mainScrollTop.value,
|
width: componentWidth,
|
height: componentHeight
|
})
|
const tabComponentId = element.value.canvasId.split('--')[0]
|
dvMainStore.setTabMoveOutComponentId(tabComponentId)
|
} else {
|
dvMainStore.setTabMoveOutComponentId(null)
|
contentDisplay.value = true
|
}
|
// 仪表板进行Tab碰撞检查
|
tabMoveInCheck()
|
// 仪表板模式 会造成移动现象 当检测组件正在碰撞有效区内或者移入有效区内 则周边组件不进行移动
|
if (
|
dashboardActive.value &&
|
(isFirst || (!tabMoveInActiveId.value && !tabCollisionActiveId.value))
|
) {
|
element.value['dragging'] = true
|
emit('onDragging', e)
|
}
|
|
//如果当前组件是Group分组 则要进行内部组件深度计算
|
if (['DeTabs', 'Group'].includes(element.value.component)) {
|
groupSizeStyleAdaptor(element.value)
|
}
|
//如果当前画布是Group内部画布 则对应组件定位在resize时要还原到groupStyle中
|
if (isGroupCanvas(canvasId.value) || isTabCanvas(canvasId.value)) {
|
groupStyleRevert(element.value, {
|
width: parentNode.value.offsetWidth,
|
height: parentNode.value.offsetHeight
|
})
|
}
|
// 防止首次组件在tab旁边无法触发矩阵移动
|
if (isFirst) {
|
isFirst = false
|
}
|
// 修改当前组件样式
|
dvMainStore.setShapeStyle(pos, areaData.value.components, 'move')
|
// 等更新完当前组件的样式并绘制到屏幕后再判断是否需要吸附
|
// GroupArea是分组视括组件 不需要进行吸附
|
// 如果不使用 nextTick,吸附后将无法移动
|
if (!isGroupArea.value) {
|
nextTick(() => {
|
// 触发元素移动事件,用于显示标线、吸附功能
|
// 后面两个参数代表鼠标移动方向
|
// curY - startY > 0 true 表示向下移动 false 表示向上移动
|
// curX - startX > 0 true 表示向右移动 false 表示向左移动
|
eventBus.emit('move', { isDownward: curY - startY > 0, isRightward: curX - startX > 0 })
|
})
|
}
|
|
lastTime = now
|
rafId = null
|
})
|
}
|
|
const up = () => {
|
// 清理动画帧
|
if (rafId) {
|
cancelAnimationFrame(rafId)
|
rafId = null
|
}
|
|
dashboardActive.value && emit('onMouseUp')
|
element.value['dragging'] = false
|
hasMove &&
|
snapshotStore.recordSnapshotCacheWithPositionChange('shape-handleMouseDownOnShape-up')
|
// 触发元素停止移动事件,用于隐藏标线
|
eventBus.emit('unMove')
|
document.removeEventListener('mousemove', move)
|
document.removeEventListener('mouseup', up)
|
|
//如果当前存在移入的Tab 则将该组件加入到tab中 同时将该组件在主画布中进行删除
|
if (tabMoveInActiveId.value) {
|
eventBus.emit('onTabMoveIn-' + tabMoveInActiveId.value, element.value)
|
dvMainStore.setTabMoveInActiveId(null)
|
dvMainStore.setTabCollisionActiveId(null)
|
}
|
|
//如果当前存在移出的Tab 则将该组件加入到主画布中 同时将该组件在tab画布中进行删除
|
if (tabMoveOutComponentId.value) {
|
eventBus.emit('onTabMoveOut-' + tabMoveOutComponentId.value, deepCopy(element.value))
|
dvMainStore.setTabMoveOutComponentId(null)
|
}
|
handleGroupComponent()
|
}
|
|
document.addEventListener('mousemove', move)
|
document.addEventListener('mouseup', up)
|
}
|
|
const selectCurComponent = e => {
|
if (!canvasActive.value) {
|
return
|
}
|
// 阻止向父组件冒泡
|
if (dvInfo.value.type === 'dataV') {
|
e.stopPropagation()
|
e.preventDefault()
|
contextmenuStore.hideContextMenu()
|
}
|
}
|
|
const batchSelected = e => {
|
if (batchOptFlag.value) {
|
componentEditBarRef.value.batchOptCheckOut()
|
e.stopPropagation()
|
e.preventDefault()
|
return
|
}
|
}
|
|
const handleMouseDownOnPoint = (point, e) => {
|
if (!canvasActive.value) {
|
return
|
}
|
dashboardActive.value && emit('onStartResize', e)
|
dvMainStore.setInEditorStatus(true)
|
dvMainStore.setClickComponentStatus(true)
|
e.stopPropagation()
|
e.preventDefault()
|
|
const style = { ...defaultStyle.value }
|
|
// 组件宽高比
|
const proportion = style['width'] / style['height']
|
|
// 组件中心点
|
const center = {
|
x: style['left'] + style['width'] / 2,
|
y: style['top'] + style['height'] / 2
|
}
|
|
// 获取画布位移信息
|
const editorRectInfo = editorMap.value[canvasId.value]?.getBoundingClientRect()
|
if (!editorRectInfo) {
|
return
|
}
|
// 获取 point 与实际拖动基准点的差值
|
const pointRect = e.target.getBoundingClientRect()
|
// 当前点击圆点相对于画布的中心坐标
|
const curPoint = {
|
x: Math.round(
|
pointRect.left -
|
editorRectInfo.left +
|
e.target.offsetWidth / 2 +
|
offsetGapAdaptor('x', point) / 2
|
),
|
y: Math.round(
|
pointRect.top -
|
editorRectInfo.top +
|
e.target.offsetHeight / 2 +
|
offsetGapAdaptor('y', point) / 2
|
)
|
}
|
|
// 获取对称点的坐标 problem point
|
const symmetricPoint = {
|
x: center.x - (curPoint.x - center.x) - offsetGapAdaptor('x', point) / 4,
|
y: center.y - (curPoint.y - center.y) - offsetGapAdaptor('y', point) / 4
|
}
|
|
// 是否需要保存快照
|
let needSave = false
|
let isFirst = true
|
|
const needLockProportion = isNeedLockProportion()
|
const originRadio = curComponent.value.aspectRatio
|
const baseGroupComponentsRadio = {}
|
// 计算初始状态 分组内组件宽高占比
|
if (areaData.value.components.length > 0) {
|
areaData.value.components.forEach(groupComponent => {
|
baseGroupComponentsRadio[groupComponent.id] = {
|
topRadio: (groupComponent.style.top - style.top) / style.height,
|
leftRadio: (groupComponent.style.left - style.left) / style.width,
|
widthRadio: groupComponent.style.width / style.width,
|
heightRadio: groupComponent.style.height / style.height
|
}
|
})
|
}
|
|
const move = moveEvent => {
|
// 第一次点击时也会触发 move,所以会有“刚点击组件但未移动,组件的大小却改变了”的情况发生
|
// 因此第一次点击时不触发 move 事件
|
if (isFirst) {
|
isFirst = false
|
return
|
}
|
|
needSave = true
|
const curPosition = {
|
x: moveEvent.clientX - Math.round(editorRectInfo.left) + offsetGapAdaptor('x', point),
|
y: moveEvent.clientY - Math.round(editorRectInfo.top) + offsetGapAdaptor('y', point)
|
}
|
calculateComponentPositionAndSize(point, style, curPosition, proportion, needLockProportion, {
|
center,
|
curPoint,
|
symmetricPoint
|
})
|
//Temp dataV坐标偏移
|
offsetDataVAdaptor(style, point)
|
// 保持宽搞比例调整
|
if (curComponent.value.maintainRadio) {
|
// 高度偏移量
|
const heightOffset = style.height - defaultStyle.value.height
|
// 宽度偏移量
|
const widthOffset = style.width - defaultStyle.value.width
|
// 保持宽高比例是相对宽度偏移量
|
const adaptorWidthOffset = heightOffset * originRadio
|
if (pointCorner.includes(point)) {
|
style.height = defaultStyle.value.width / originRadio
|
} else if (Math.abs(widthOffset) > Math.abs(adaptorWidthOffset)) {
|
// 调整高度
|
style.height = defaultStyle.value.width / originRadio
|
} else {
|
// 调整宽度
|
style.width = defaultStyle.value.height * originRadio
|
}
|
}
|
calculateRadioComponentPositionAndSize(point, style, symmetricPoint)
|
|
dvMainStore.setShapeStyle(style, areaData.value.components, 'resize', baseGroupComponentsRadio)
|
// 矩阵逻辑 如果当前是仪表板(矩阵模式)则要进行矩阵重排
|
dashboardActive.value && emit('onResizing', moveEvent)
|
element.value['resizing'] = true
|
//如果当前组件是Group分组或者Tab 则要进行内部组件深度计算
|
if (
|
['Group'].includes(element.value.component) ||
|
(['DeTabs'].includes(element.value.component) && !element.value.resizeInnerKeep)
|
) {
|
groupSizeStyleAdaptor(element.value)
|
}
|
|
//如果当前画布是Group内部画布 则对应组件定位在resize时要还原到groupStyle中
|
if (isGroupCanvas(canvasId.value) || isTabCanvas(canvasId.value)) {
|
groupStyleRevert(element.value, {
|
width: parentNode.value.offsetWidth,
|
height: parentNode.value.offsetHeight
|
})
|
}
|
}
|
|
const up = () => {
|
// 如果内部组件保持尺寸时,这里在鼠标抬起时,重新计算一下内部组件占比
|
if (['DeTabs'].includes(element.value.component) && element.value.resizeInnerKeep) {
|
tabInnerStyleRevert(element.value)
|
}
|
|
dashboardActive.value && emit('onMouseUp')
|
element.value['resizing'] = false
|
document.removeEventListener('mousemove', move)
|
document.removeEventListener('mouseup', up)
|
needSave &&
|
snapshotStore.recordSnapshotCacheWithPositionChange('shape-handleMouseDownOnPoint-up')
|
handleGroupComponent()
|
}
|
|
document.addEventListener('mousemove', move)
|
document.addEventListener('mouseup', up)
|
}
|
|
const offsetDataVAdaptor = (style, point) => {
|
if (!dashboardActive.value) {
|
const offset = 1
|
if (point.indexOf('r') > -1) {
|
style['left'] = style['left'] + offset
|
}
|
if (point.indexOf('b') > -1) {
|
style['top'] = style['top'] + offset
|
}
|
if (point.indexOf('l') > -1) {
|
style['width'] = style['width'] + offset
|
}
|
if (point.indexOf('t') > -1) {
|
style['height'] = style['height'] + offset
|
}
|
}
|
}
|
|
// resize算法适配,根据9个拖转点的位置 调整curGap 引起的中心点centerPoint 圆点curPoint 对称点 symmetricPoint引起的偏移
|
const offsetGapAdaptor = (dimension, point) => {
|
const offset = baseCellInfo.value.curGap
|
if (dimension === 'x') {
|
return point.indexOf('r') > -1 ? offset : -1 * offset
|
} else {
|
return point.indexOf('b') > -1 ? offset : -1 * offset
|
}
|
}
|
|
const isNeedLockProportion = () => {
|
if (element.value.component != 'Group') return false
|
const rotates = [0, 90, 180, 360]
|
for (const component of element.value.propValue) {
|
if (!rotates.includes(mod360(parseInt(component.style.rotate)))) {
|
return true
|
}
|
}
|
|
return false
|
}
|
|
const svgInnerEnable = computed(() => {
|
const { backgroundImageEnable, backgroundType, innerImage } = element.value.commonBackground
|
return backgroundImageEnable && backgroundType === 'innerImage' && typeof innerImage === 'string'
|
})
|
|
const commonBackgroundSvgInner = computed(() => {
|
if (svgInnerEnable.value) {
|
return element.value.commonBackground.innerImage.replace('board/', '').replace('.svg', '')
|
} else {
|
return null
|
}
|
})
|
|
const componentBackgroundStyle = computed(() => {
|
if (element.value.commonBackground && element.value.component !== 'GroupArea') {
|
const {
|
backdropFilterEnable,
|
backdropFilter,
|
backgroundColorSelect,
|
backgroundColor,
|
backgroundImageEnable,
|
backgroundType,
|
outerImage,
|
innerPadding,
|
borderRadius
|
} = element.value.commonBackground
|
const commonBackground = element.value.commonBackground as CommonBackground
|
const innerPaddingTarget = ['Group'].includes(element.value.component) ? 0 : innerPadding
|
let innerPaddingStyle = innerPaddingTarget * scale.value + 'px'
|
const paddingMode = commonBackground.innerPadding?.mode
|
if (paddingMode === ShorthandMode.Uniform) {
|
innerPaddingStyle = `${commonBackground.innerPadding?.top * scale.value}px`
|
} else if (paddingMode === ShorthandMode.Axis) {
|
innerPaddingStyle = `${commonBackground.innerPadding?.top * scale.value}px ${
|
commonBackground.innerPadding?.left * scale.value
|
}px`
|
} else if (paddingMode === ShorthandMode.PerEdge) {
|
innerPaddingStyle = `${commonBackground.innerPadding?.top * scale.value}px ${
|
commonBackground.innerPadding?.right * scale.value
|
}px ${commonBackground.innerPadding?.bottom * scale.value}px ${
|
commonBackground.innerPadding?.left * scale.value
|
}px`
|
}
|
|
let borderRadiusStyle = borderRadius + 'px'
|
const borderRadiusMode = commonBackground.borderRadius?.mode
|
if (borderRadiusMode === ShorthandMode.Uniform) {
|
borderRadiusStyle = `${commonBackground.borderRadius?.topLeft * scale.value}px`
|
} else if (borderRadiusMode === ShorthandMode.Axis) {
|
borderRadiusStyle = `${commonBackground.borderRadius?.topLeft * scale.value}px ${
|
commonBackground.borderRadius?.bottomLeft * scale.value
|
}px`
|
} else if (borderRadiusMode === ShorthandMode.PerEdge) {
|
borderRadiusStyle = `${commonBackground.borderRadius?.topLeft * scale.value}px ${
|
commonBackground.borderRadius?.topRight * scale.value
|
}px ${commonBackground.borderRadius?.bottomRight * scale.value}px ${
|
commonBackground.borderRadius?.bottomLeft * scale.value
|
}px`
|
}
|
|
let style = {
|
padding: innerPaddingStyle,
|
borderRadius: borderRadiusStyle
|
}
|
let colorRGBA = ''
|
if (backgroundColorSelect && backgroundColor) {
|
colorRGBA = backgroundColor
|
}
|
if (backgroundImageEnable) {
|
if (backgroundType === 'outerImage' && typeof outerImage === 'string') {
|
style['background'] = `url(${imgUrlTrans(outerImage)}) no-repeat ${colorRGBA}`
|
} else {
|
style['background-color'] = colorRGBA
|
}
|
} else {
|
style['background-color'] = colorRGBA
|
}
|
if (element.value.component !== 'UserView') {
|
style['overflow'] = 'hidden'
|
}
|
if (backdropFilterEnable) {
|
style['backdrop-filter'] = 'blur(' + backdropFilter + 'px)'
|
}
|
return style
|
}
|
return {}
|
})
|
|
const editBarShowFlag = computed(() => {
|
return (
|
((active.value || batchOptFlag.value) &&
|
['canvas', 'canvasDataV', 'batchOpt'].includes(showPosition.value)) ||
|
linkageSettingStatus.value
|
)
|
})
|
|
const linkageActive = computed(() => {
|
return linkageSettingStatus.value && element.value.id === curLinkageView.value.id
|
})
|
|
const linkJumpSetOpen = () => {
|
emit('linkJumpSetOpen')
|
}
|
const linkageSetOpen = () => {
|
emit('linkageSetOpen')
|
}
|
|
// 设置属性(属性跟随所属canvas component类型 要做出改变)
|
const settingAttribute = () => {
|
// 设置Tab移入检测
|
shapeInnerRef.value.setAttribute('tab-is-check', `${isTabMoveCheck.value}`)
|
// 设置组件类型
|
shapeInnerRef.value.setAttribute('component-type', `${element.value.component}`)
|
// 设置组件ID
|
shapeInnerRef.value.setAttribute('component-id', `${element.value.id}`)
|
}
|
|
const tabMoveInCheck = async () => {
|
const curNode = document.querySelector('#' + domId.value)
|
const width = curNode.offsetWidth
|
const height = curNode.offsetHeight
|
const left = curNode.offsetLeft
|
const top = curNode.offsetTop
|
// tab 移入检测开启 tab组件不能相互移入另一个tab组件
|
// 如当前是分组且分组中含有Tab 不允许移入 pJoinTab = false
|
if (
|
pJoinTab &&
|
isTabMoveCheck.value &&
|
!state.ignoreTabMoveComponent.includes(element.value.component)
|
) {
|
const nodes = Array.from(parentNode.value.childNodes) // 获取当前父节点下所有子节点
|
for (const item of nodes) {
|
if (
|
item.className !== undefined &&
|
typeof item.className === 'string' &&
|
item.className.split(' ').includes('shape') &&
|
item.getAttribute('component-id') !== domId.value && // 去掉当前
|
item.getAttribute('tab-is-check') !== null &&
|
item.getAttribute('tab-is-check') !== 'false' &&
|
item.getAttribute('component-type') === 'DeTabs'
|
) {
|
const componentId = item.getAttribute('component-id')
|
|
const tw = item.offsetWidth
|
const th = item.offsetHeight
|
const tl = item.offsetLeft
|
const tt = item.offsetTop
|
|
// 碰撞有效区域检查
|
const collisionT = tt + state.tabMoveInYOffset
|
const collisionL = tl + state.collisionGap - width
|
const collisionW = tw + 2 * width - state.collisionGap
|
const collisionH = th + height - state.tabMoveInYOffset
|
|
// 左上角靠近左上角区域
|
const tfAndTf = collisionT <= top && collisionL <= left
|
// 左下角靠近左下角区域
|
const bfAndBf = collisionT + collisionH >= top + height && collisionL <= left
|
// 右上角靠近右上角区域
|
const trAndTr = collisionT <= top && collisionL + collisionW >= left + width
|
// 右下角靠近右下角区域
|
const brAndBr =
|
collisionT + collisionH >= top + height && collisionL + collisionW >= left + width
|
if (tfAndTf && bfAndBf && trAndTr && brAndBr) {
|
dvMainStore.setTabCollisionActiveId(componentId)
|
} else if (tabCollisionActiveId.value === componentId) {
|
dvMainStore.setTabCollisionActiveId(null)
|
}
|
|
// 移入有效区域检查
|
// 碰撞有效区域检查
|
const activeT = tt + state.tabMoveInYOffset
|
const activeL = tl + state.collisionGap * 10 - width
|
const activeW = tw + 2 * width - state.collisionGap * 20
|
const activeH = th + height - 2 * state.tabMoveInYOffset
|
|
// 左上角靠近左上角区域
|
const activeTfAndTf = activeT <= top && activeL <= left
|
// 左下角靠近左下角区域
|
const activeBfAndBf = activeT + activeH >= top + height && activeL <= left
|
// 右上角靠近右上角区域
|
const activeTrAndTr = activeT <= top && activeL + activeW >= left + width
|
// 右下角靠近右下角区域
|
const activeBrAndBr = activeT + activeH >= top + height && activeL + activeW >= left + width
|
if (activeTfAndTf && activeBfAndBf && activeTrAndTr && activeBrAndBr) {
|
dvMainStore.setTabMoveInActiveId(componentId)
|
} else if (tabMoveInActiveId.value === componentId) {
|
dvMainStore.setTabMoveInActiveId(null)
|
}
|
}
|
}
|
}
|
}
|
const slotStyle = computed(() => {
|
// 3d效果支持
|
if (element.value['multiDimensional'] && element.value['multiDimensional']?.enable) {
|
const width = defaultStyle.value.width // 原始元素宽度
|
const height = defaultStyle.value.height // 原始元素高度
|
const rotateX = element.value['multiDimensional'].x // 旋转X角度
|
const rotateY = element.value['multiDimensional'].y // 旋转Y角度
|
|
// 将角度转换为弧度
|
const radX = (rotateX * Math.PI) / 180
|
const radY = (rotateY * Math.PI) / 180
|
|
// 计算旋转后新宽度和高度
|
const newWidth = Math.abs(width * Math.cos(radY)) + Math.abs(height * Math.sin(radX))
|
const newHeight = Math.abs(height * Math.cos(radX)) + Math.abs(width * Math.sin(radY))
|
|
// 计算需要的 padding
|
const paddingX = (newWidth - width) / 2
|
const paddingY = (newHeight - height) / 2
|
|
return {
|
padding: `${paddingY}px ${paddingX}px`,
|
transform: `rotateX(${element.value['multiDimensional'].x}deg) rotateY(${element.value['multiDimensional'].y}deg) rotateZ(${element.value['multiDimensional'].z}deg)`
|
}
|
} else {
|
return {}
|
}
|
})
|
|
const batchOptFlag = computed(() => {
|
return batchOptStatus.value && dashboardActive.value
|
})
|
|
const dragCollision = computed(() => {
|
return active.value && Boolean(tabCollisionActiveId.value)
|
})
|
|
const htmlToImage = () => {
|
downLoading.value = true
|
useEmitt().emitter.emit('l7-prepare-picture', element.value.id)
|
setTimeout(() => {
|
activeWatermarkCheckUser(viewDemoInnerId.value, 'canvas-main', scale.value)
|
const dom = document.getElementById(viewDemoInnerId.value)
|
downloadCanvas2('img', dom, '图表', () => {
|
// do callback
|
removeActiveWatermark(viewDemoInnerId.value)
|
downLoading.value = false
|
useEmitt().emitter.emit('l7-unprepare-picture', element.value.id)
|
})
|
}, 200)
|
}
|
|
const handleGroupComponent = () => {
|
if (element.value.canvasId.includes('Group')) {
|
composeStore.updateGroupBorder(element.value.canvasId)
|
}
|
}
|
|
onMounted(() => {
|
parentNode.value = document.querySelector('#editor-' + canvasId.value)
|
// 用于 Group 组件
|
if (curComponent.value) {
|
cursors.value = getCursor() // 根据旋转角度获取光标位置
|
}
|
eventBus.on('runAnimation', () => {
|
if (element.value == curComponent.value) {
|
}
|
})
|
eventBus.on('stopAnimation', () => {
|
// do stopAnimation
|
})
|
settingAttribute()
|
const methodName = 'componentImageDownload-' + element.value.id
|
if (!useEmitt().emitter.all.get(methodName)?.length) {
|
useEmitt().emitter.on(methodName, () => {
|
htmlToImage()
|
})
|
}
|
})
|
</script>
|
|
<style lang="less" scoped>
|
.shape {
|
position: absolute;
|
.refresh-from-pc {
|
position: absolute;
|
right: 38px;
|
top: 12px;
|
z-index: 2;
|
font-size: 16px;
|
cursor: pointer;
|
color: var(--ed-color-primary);
|
}
|
.del-from-mobile {
|
position: absolute;
|
right: 12px;
|
top: 12px;
|
z-index: 2;
|
font-size: 16px;
|
cursor: pointer;
|
color: var(--ed-color-primary);
|
}
|
}
|
|
.shape-group-area {
|
z-index: 15;
|
}
|
|
.shape-shadow {
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
position: absolute;
|
background-size: 100% 100% !important;
|
}
|
|
.shape-inner {
|
width: 100%;
|
height: 100%;
|
position: relative;
|
background-size: 100% 100% !important;
|
}
|
|
.shape-selected {
|
outline: 1px solid var(--ed-color-primary, #3370ff);
|
}
|
|
.shape-edit {
|
&:hover {
|
cursor: move;
|
outline: 1px solid var(--ed-color-primary-99, rgba(51, 112, 255, 0.6));
|
}
|
}
|
|
.shape-lock {
|
&:hover {
|
cursor: not-allowed !important;
|
}
|
}
|
|
.active {
|
outline: 1px solid var(--ed-color-primary) !important;
|
user-select: none;
|
}
|
|
.shape-point {
|
position: absolute;
|
background: #fff;
|
border: 1px solid #59c7f9;
|
width: 8px;
|
height: 8px;
|
border-radius: 50%;
|
z-index: 0;
|
}
|
|
.icon-xiangyouxuanzhuan {
|
position: absolute;
|
top: -34px;
|
left: 50%;
|
transform: translateX(-50%);
|
cursor: grab;
|
color: #59c7f9;
|
font-size: 20px;
|
font-weight: 600;
|
|
&:active {
|
cursor: grabbing;
|
}
|
}
|
|
.icon-suo {
|
width: 22px;
|
height: 22px;
|
z-index: 1;
|
position: absolute;
|
top: 0;
|
right: 0;
|
}
|
|
.svg-background {
|
position: absolute;
|
z-index: 0;
|
top: 0;
|
left: 0;
|
width: 100% !important;
|
height: 100% !important;
|
}
|
|
.shape-outer {
|
width: 100%;
|
height: 100%;
|
position: relative;
|
.edit-bar {
|
display: none;
|
}
|
}
|
|
.linkage-setting {
|
opacity: 0.5;
|
}
|
|
.drag-on-tab-collision {
|
z-index: 1000 !important;
|
}
|
.edit-bar-active {
|
display: inline-block !important;
|
}
|
|
.de-drag-area {
|
position: absolute;
|
}
|
|
.de-drag-area:hover {
|
cursor: move;
|
}
|
|
.de-drag-top {
|
left: 1px;
|
top: 1px;
|
height: 12px;
|
width: calc(100% - 2px);
|
}
|
|
.de-drag-right {
|
right: 1px;
|
top: 70px;
|
width: 12px;
|
height: calc(100% - 110px);
|
}
|
|
.de-drag-bottom {
|
left: 100px;
|
bottom: 1px;
|
height: 12px;
|
width: calc(100% - 102px);
|
}
|
|
.de-drag-left {
|
left: 1px;
|
top: 1px;
|
width: 16px;
|
height: calc(100% - 40px);
|
}
|
|
.component-slot {
|
width: 100%;
|
height: 100%;
|
position: relative;
|
transform-style: preserve-3d;
|
}
|
|
.freeze-component {
|
position: fixed;
|
z-index: 1;
|
top: 66px !important;
|
}
|
|
.freeze-component-fullscreen {
|
top: 5px !important;
|
}
|
</style>
|