<script setup lang="ts">
import dvRuler from '@/assets/svg/dv-ruler.svg'
import CanvasAttr from '@/components/data-visualization/CanvasAttr.vue'
import { computed, watch, onMounted, reactive, ref, nextTick, onUnmounted } 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 { useAppStoreWithOut } from '@/store/modules/app'
import { storeToRefs } from 'pinia'
import DvToolbar from '../../components/data-visualization/DvToolbar.vue'
import ComponentToolBar from '../../components/data-visualization/ComponentToolBar.vue'
import eventBus from '../../utils/eventBus'
import { findComponentAttr } from '../../utils/components'
import DvSidebar from '../../components/visualization/DvSidebar.vue'
import router from '@/router'
import Editor from '@/views/chart/components/editor/index.vue'
import { guid } from '@/views/visualized/data/dataset/form/util.js'
import { getDatasetTree } from '@/api/dataset'
import { Tree } from '@/views/visualized/data/dataset/form/CreatDsGroup.vue'
import {
decompressionPre,
findDragComponent,
findNewComponent,
initCanvasData,
onInitReady
} from '@/utils/canvasUtils'
import CanvasCore from '@/components/data-visualization/canvas/CanvasCore.vue'
import { listenGlobalKeyDown, releaseAttachKey } from '@/utils/DeShortcutKey'
import { adaptCurThemeCommonStyle } from '@/utils/canvasStyle'
import { useEmbedded } from '@/store/modules/embedded'
import { changeComponentSizeWithScale } from '@/utils/changeComponentsSizeWithScale'
import { useEmitt } from '@/hooks/web/useEmitt'
import { check, compareStorage } from '@/utils/CrossPermission'
import { useCache } from '@/hooks/web/useCache'
import RealTimeListTree from '@/components/data-visualization/RealTimeListTree.vue'
import { interactiveStoreWithOut } from '@/store/modules/interactive'
import { watermarkFind } from '@/api/watermark'
import { XpackComponent } from '@/components/plugin'
import { Base64 } from 'js-base64'
import CanvasCacheDialog from '@/components/visualization/CanvasCacheDialog.vue'
import { deepCopy } from '@/utils/utils'
import DvPreview from '@/views/data-visualization/DvPreview.vue'
import DeRuler from '@/custom-component/common/DeRuler.vue'
import { useRequestStoreWithOut } from '@/store/modules/request'
import { usePermissionStoreWithOut } from '@/store/modules/permission'
import ChartStyleBatchSet from '@/views/chart/components/editor/editor-style/ChartStyleBatchSet.vue'
import CustomTabsSort from '@/custom-component/de-tabs/CustomTabsSort.vue'
import { useI18n } from '@/hooks/web/useI18n'
import { recoverToPublished } from '@/api/visualization/dataVisualization'
const interactiveStore = interactiveStoreWithOut()
const embeddedStore = useEmbedded()
const { wsCache } = useCache()
const dvPreviewRef = ref(null)
const { t } = useI18n()
const eventCheck = e => {
if (e.key === 'screen-weight' && !compareStorage(e.oldValue, e.newValue)) {
const opt = embeddedStore.opt || router.currentRoute.value.query.opt
if (!(opt && opt === 'create')) {
check(
wsCache.get('screen-weight'),
embeddedStore.dvId || (router.currentRoute.value.query.dvId as string),
4
)
}
}
}
const mainCanvasCoreRef = ref(null)
const customTabsSortRef = ref(null)
const appStore = useAppStoreWithOut()
const isDataEaseBi = computed(() => appStore.getIsDataEaseBi)
const dvMainStore = dvMainStoreWithOut()
const snapshotStore = snapshotStoreWithOut()
const contextmenuStore = contextmenuStoreWithOut()
const composeStore = composeStoreWithOut()
const canvasCacheOutRef = ref(null)
const deWRulerRef = ref(null)
const deHRulerRef = ref(null)
const requestStore = useRequestStoreWithOut()
const permissionStore = usePermissionStoreWithOut()
const {
fullscreenFlag,
componentData,
curComponent,
isClickComponent,
canvasStyleData,
canvasViewInfo,
editMode,
dvInfo,
canvasState,
batchOptStatus
} = storeToRefs(dvMainStore)
const { editorMap, isSpaceDown } = storeToRefs(composeStore)
const canvasOut = ref(null)
const canvasInner = ref(null)
const leftSidebarRef = ref(null)
const dvLayout = ref(null)
const canvasCenterRef = ref(null)
const mainHeight = ref(300)
let createType = null
let isDragging = false // 标记是否在拖动
let startX, startY, scrollLeft, scrollTop
const state = reactive({
sideShow: true,
countTime: 0,
datasetTree: [],
scaleHistory: null,
canvasId: 'canvas-main',
canvasInitStatus: false,
sourcePid: null,
resourceId: null,
opt: null,
baseWidth: 10,
baseHeight: 10
})
const tabSort = () => {
if (curComponent.value) {
customTabsSortRef.value.sortInit(curComponent.value)
}
}
// 启用拖动
const enableDragging = e => {
if (isSpaceDown.value) {
// 仅在空格键按下时启用拖动
isDragging = true
startX = e.pageX - canvasOut.value.wrapRef.offsetLeft
startY = e.pageY - canvasOut.value.wrapRef.offsetTop
scrollLeft = canvasOut.value.wrapRef.scrollLeft
scrollTop = canvasOut.value.wrapRef.scrollTop
e.preventDefault()
e.stopPropagation()
}
}
// 执行拖动
const onMouseMove = e => {
if (!isDragging) return
e.preventDefault()
e.stopPropagation()
const x = e.pageX - canvasOut.value.wrapRef.offsetLeft
const y = e.pageY - canvasOut.value.wrapRef.offsetTop
const walkX = x - startX
const walkY = y - startY
canvasOut.value.wrapRef.scrollLeft = scrollLeft - walkX
canvasOut.value.wrapRef.scrollTop = scrollTop - walkY
}
// 禁用拖动
const disableDragging = () => {
isDragging = false
}
const contentStyle = computed(() => {
const { width, height } = canvasStyleData.value
if (editMode.value === 'preview') {
return {
width: '100%',
height: 'auto',
overflow: 'hidden'
}
} else {
return {
minWidth: '1600px',
width: width * scrollOffset.value + 'px',
height: height * scrollOffset.value + 'px'
}
}
})
// 通过实时监听的方式直接添加组件
const handleNew = newComponentInfo => {
state.countTime++
const { componentName, innerType, staticMap } = newComponentInfo
if (componentName) {
const { width, height, scale } = canvasStyleData.value
const component = findNewComponent(componentName, innerType, staticMap)
component.style.top = ((height - component.style.height) * scale) / 200
component.style.left = ((width - component.style.width) * scale) / 200
component.id = guid()
const popComponents = componentData.value.filter(
ele => ele.category && ele.category === 'hidden'
)
// 弹框区域组件 只允许有一个过滤组件
if (
canvasState.value.curPointArea === 'hidden' &&
component.component === 'VQuery' &&
(!popComponents || popComponents.length === 0)
) {
component.category = canvasState.value.curPointArea
component.commonBackground.backgroundColor = 'rgba(41, 41, 41, 1)'
}
changeComponentSizeWithScale(component)
dvMainStore.addComponent({ component: component, index: undefined })
adaptCurThemeCommonStyle(component)
snapshotStore.recordSnapshotCache('renderChart', component.id)
if (state.countTime > 10) {
state.sideShow = false
nextTick(() => {
state.countTime = 0
state.sideShow = true
})
}
useEmitt().emitter.emit('initScroll')
}
}
const handleDrop = e => {
e.preventDefault()
e.stopPropagation()
const componentInfo = e.dataTransfer.getData('id')
const rectInfo = editorMap.value[state.canvasId].getBoundingClientRect()
if (componentInfo) {
const component = findDragComponent(componentInfo)
component.style.top = e.clientY - rectInfo.y
component.style.left = e.clientX - rectInfo.x
component.id = guid()
changeComponentSizeWithScale(component)
dvMainStore.addComponent({ component: component, index: undefined })
adaptCurThemeCommonStyle(component)
snapshotStore.recordSnapshotCache('renderChart', component.id)
}
}
const handleDragOver = e => {
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
}
const handleMouseDown = e => {
// e.stopPropagation()
if (isSpaceDown.value) {
return
}
dvMainStore.setClickComponentStatus(false)
// 点击画布的空区域 提前清空curComponent 防止右击菜单内容抖动
dvMainStore.setCurComponent({ component: null, index: null })
dvMainStore.setInEditorStatus(true)
mainCanvasCoreRef.value.handleMouseDown(e)
}
const deselectCurComponent = e => {
if (isSpaceDown.value) {
return
}
if (!isClickComponent.value) {
curComponent.value && dvMainStore.setCurComponent({ component: null, index: null })
}
// 0 左击 1 滚轮 2 右击
if (e.button != 2) {
contextmenuStore.hideContextMenu()
}
}
const initDataset = () => {
getDatasetTree({}).then(res => {
state.datasetTree = (res as unknown as Tree[]) || []
})
}
const scrollOffset = computed(() => (canvasStyleData.value.scale < 150 ? 1.5 : 2))
// 全局监听按键事件
listenGlobalKeyDown()
const initScroll = () => {
nextTick(() => {
if (canvasCenterRef.value) {
const { width, height } = canvasStyleData.value
const mainWidth = canvasCenterRef.value.clientWidth
mainHeight.value = canvasCenterRef.value.clientHeight
const scrollX = (scrollOffset.value * width - mainWidth) / 2
const scrollY = (scrollOffset.value * height - mainHeight.value) / 2 + 20
// 设置画布初始滚动条位置
canvasOut.value.scrollTo(scrollX, scrollY)
}
})
}
const doUseCache = flag => {
const canvasCache = wsCache.get('DE-DV-CATCH-' + state.resourceId)
if (flag && canvasCache) {
const canvasCacheSeries = deepCopy(canvasCache)
snapshotStore.snapshotPublish(canvasCacheSeries)
state.canvasInitStatus = true
nextTick(() => {
dvMainStore.setDataPrepareState(true)
snapshotStore.recordSnapshotCache('renderChart')
setTimeout(() => {
// 使用缓存时,初始化的保存按钮为激活状态
snapshotStore.recordSnapshotCache('renderChart')
}, 2000)
})
} else {
initLocalCanvasData(() => {
// do init
})
wsCache.delete('DE-DV-CATCH-' + state.resourceId)
}
}
const initLocalCanvasData = async callback => {
const { opt, sourcePid, resourceId } = state
const busiFlag = opt === 'copy' ? 'dataV-copy' : 'dataV'
await initCanvasData(
resourceId,
{ busiFlag, resourceTable: 'snapshot', source: 'main-edit' },
function () {
state.canvasInitStatus = true
// afterInit
nextTick(() => {
dvMainStore.setDataPrepareState(true)
snapshotStore.recordSnapshotCache('renderChart')
if (dvInfo.value && opt === 'copy') {
dvInfo.value.dataState = 'prepare'
dvInfo.value.optType = 'copy'
dvInfo.value.pid = sourcePid
setTimeout(() => {
snapshotStore.recordSnapshotCache('renderChart')
}, 1500)
}
onInitReady({ resourceId: resourceId })
callback && callback()
})
}
)
}
const previewScaleChange = () => {
state.scaleHistory = canvasStyleData.value.scale
nextTick(() => {
let canvasWidth = dvLayout.value.clientWidth
const previewScale = (canvasWidth * 100) / canvasStyleData.value.width
canvasStyleData.value.scale = previewScale
})
}
watch(
() => editMode.value,
val => {
if (val === 'edit') {
if (state.scaleHistory) {
canvasStyleData.value.scale = state.scaleHistory
}
initScroll()
} else {
previewScaleChange()
}
}
)
const checkPer = async resourceId => {
if (!window.DataEaseBi || !resourceId) {
return true
}
const request = { busiFlag: 'dataV', resourceTable: 'core' }
await interactiveStore.setInteractive(request)
return check(wsCache.get('screen-weight'), resourceId, 4)
}
// 目标校验: 需要校验targetSourceId 是否是当前可视化资源ID
const winMsgHandle = event => {
const msgInfo = event.data
if (msgInfo?.targetSourceId === dvInfo.value.id + '')
if (msgInfo.type === 'webParams') {
// 网络消息处理
winMsgWebParamsHandle(msgInfo)
}
}
const winMsgWebParamsHandle = msgInfo => {
const params = msgInfo.params
dvMainStore.addWebParamsFilter(params)
}
const loadFinish = ref(false)
const newWindowFromDiv = ref(false)
let p = null
const XpackLoaded = () => p(true)
onMounted(async () => {
document.body.style.overflow = 'hidden'
dvMainStore.setCurComponent({ component: null, index: null })
snapshotStore.initSnapShot()
if (window.location.hash.includes('#/dvCanvas')) {
newWindowFromDiv.value = true
}
await new Promise(r => (p = r))
loadFinish.value = true
window.addEventListener('blur', releaseAttachKey)
window.addEventListener('message', winMsgHandle)
if (editMode.value === 'edit') {
window.addEventListener('storage', eventCheck)
}
const dvId = embeddedStore.dvId || router.currentRoute.value.query.dvId
const pid = embeddedStore.pid || router.currentRoute.value.query.pid
const templateParams =
embeddedStore.templateParams || router.currentRoute.value.query.templateParams
createType = embeddedStore.createType || router.currentRoute.value.query.createType
const opt = embeddedStore.opt || router.currentRoute.value.query.opt
const checkDvId = opt && opt === 'copy' ? null : dvId
const checkResult = await checkPer(checkDvId)
if (!checkResult) {
return
}
initDataset()
state.resourceId = dvId
state.sourcePid = pid
state.opt = opt
if (dvId) {
state.canvasInitStatus = false
const canvasCache = wsCache.get('DE-DV-CATCH-' + dvId)
if (canvasCache) {
canvasCacheOutRef.value?.dialogInit({ canvasType: 'dataV', resourceId: dvId })
} else {
await initLocalCanvasData(() => {
// do init
})
}
} else if (opt && opt === 'create') {
state.canvasInitStatus = false
let watermarkBaseInfo
try {
await watermarkFind().then(rsp => {
watermarkBaseInfo = rsp.data
watermarkBaseInfo.settingContent = JSON.parse(watermarkBaseInfo.settingContent)
})
} catch (e) {
console.error('can not find watermark info')
}
let deTemplateData
let preName
if (createType === 'template') {
const templateParamsApply = JSON.parse(Base64.decode(decodeURIComponent(templateParams + '')))
await decompressionPre(templateParamsApply, result => {
deTemplateData = result
preName = deTemplateData.baseInfo?.preName
})
}
dvMainStore.createInit('dataV', null, pid, watermarkBaseInfo, preName)
nextTick(() => {
state.canvasInitStatus = true
dvMainStore.setDataPrepareState(true)
snapshotStore.recordSnapshotCache('renderChart')
// 从模板新建
if (createType === 'template') {
dvMainStore.setComponentData(deTemplateData['componentData'])
dvMainStore.setCanvasStyle(deTemplateData['canvasStyleData'])
dvMainStore.setCanvasViewInfo(deTemplateData['canvasViewInfo'])
dvMainStore.setAppDataInfo(deTemplateData['appData'])
setTimeout(() => {
snapshotStore.recordSnapshotCache('template')
}, 1500)
}
if (dvMainStore.getAppDataInfo()) {
eventBus.emit('save')
}
})
} else {
let url = '#/screen/index'
window.open(url, '_self')
}
initScroll()
useEmitt({
name: 'initScroll',
callback: function () {
initScroll()
}
})
})
onUnmounted(() => {
document.body.style.overflow = ''
window.removeEventListener('storage', eventCheck)
window.removeEventListener('blur', releaseAttachKey)
eventBus.off('handleNew', handleNew)
eventBus.off('tabSort', tabSort)
})
const previewStatus = computed(() => editMode.value === 'preview')
const otherEditorShow = computed(() => {
return Boolean(
curComponent.value &&
(!['UserView', 'GroupArea', 'VQuery'].includes(curComponent.value?.component) ||
(curComponent.value?.component === 'UserView' &&
curComponent.value?.innerType === 'picture-group')) &&
!batchOptStatus.value
)
})
const canvasPropertiesShow = computed(
() => !curComponent.value || ['GroupArea'].includes(curComponent.value.component)
)
const viewsPropertiesShow = computed(() => {
return Boolean(
curComponent.value &&
['UserView', 'VQuery'].includes(curComponent.value.component) &&
curComponent.value.innerType !== 'picture-group' &&
!batchOptStatus.value
)
})
const scrollCanvas = e => {
deWRulerRef.value.rulerScroll(e)
deHRulerRef.value.rulerScroll(e)
}
const coreComponentData = computed(() =>
componentData.value.filter(ele => !ele.category || ele.category !== 'hidden')
)
const popComponentData = computed(() =>
componentData.value.filter(ele => ele.category && ele.category === 'hidden')
)
const doRecoverToPublished = () => {
recoverToPublished({ id: dvInfo.value.id, type: 'dataV', name: dvInfo.value.name }).then(() => {
state.resourceId = dvInfo.value.id
state.sourcePid = dvInfo.value.pid
state.opt = null
initLocalCanvasData(() => {
dvMainStore.updateDvInfoCall(1)
useEmitt().emitter.emit('calcData-all')
})
})
}
eventBus.on('handleNew', handleNew)
eventBus.on('tabSort', tabSort)
</script>
<template>
<div
ref="dvLayout"
class="dv-common-layout"
:class="isDataEaseBi && !newWindowFromDiv && 'dataease-w-h'"
>
<DvToolbar @recover-to-published="doRecoverToPublished" />
<div class="custom-dv-divider" />
<el-container
v-if="loadFinish"
v-loading="requestStore.loadingMap && requestStore.loadingMap[permissionStore.currentPath]"
element-loading-background="rgba(0, 0, 0, 0)"
class="dv-layout-container"
:class="{ 'preview-layout-container': previewStatus }"
>
<!-- 左侧组件列表 -->
<dv-sidebar
:title="t('visualization.layer_management')"
:width="180"
:scroll-width="3"
:aside-position="'left'"
:side-name="'realTimeComponent'"
class="left-sidebar"
id="dv-main-left-sidebar"
:class="{ 'preview-aside': previewStatus }"
>
<real-time-list-tree />
</dv-sidebar>
<!-- 中间画布 -->
<main id="dv-main-center" class="center" ref="canvasCenterRef">
<div class="de-ruler-icon-outer">
<el-icon class="de-ruler-icon">
<Icon name="dv-ruler"><dvRuler class="svg-icon" /></Icon>
</el-icon>
</div>
<de-ruler ref="deWRulerRef" @update:tickSize="val => (state.baseWidth = val)"></de-ruler>
<de-ruler
direction="vertical"
@update:tickSize="val => (state.baseHeight = val)"
:size="mainHeight"
ref="deHRulerRef"
></de-ruler>
<el-scrollbar
ref="canvasOut"
@scroll="scrollCanvas"
class="content"
:class="{ 'preview-content': previewStatus }"
@mousedown="enableDragging"
@mouseup="disableDragging"
@mousemove="onMouseMove"
@mouseleave="disableDragging"
>
<div
id="canvas-dv-outer"
ref="canvasInner"
:style="contentStyle"
@drop="handleDrop"
@dragover="handleDragOver"
@mousedown="handleMouseDown"
@mouseup="deselectCurComponent"
>
<div v-if="isSpaceDown" class="canvas-drag"></div>
<div class="canvas-dv-inner">
<canvas-core
class="canvas-area-shadow editor-main"
v-if="state.canvasInitStatus"
ref="mainCanvasCoreRef"
:component-data="coreComponentData"
:pop-component-data="popComponentData"
:canvas-style-data="canvasStyleData"
:canvas-view-info="canvasViewInfo"
:canvas-id="state.canvasId"
:base-height="state.baseHeight"
:base-width="state.baseWidth"
:font-family="canvasStyleData.fontFamily"
>
<template v-slot:canvasDragTips>
<div class="canvas-drag-tip">
{{ t('visualization.hold_canvas_tips') }}
</div>
</template>
</canvas-core>
</div>
</div>
</el-scrollbar>
<ComponentToolBar :class="{ 'preview-aside-x': previewStatus }"></ComponentToolBar>
</main>
<!-- 右侧侧组件列表 -->
<div style="width: auto; height: 100%" ref="leftSidebarRef">
<template v-if="!batchOptStatus && state.sideShow">
<dv-sidebar
v-if="otherEditorShow"
:title="curComponent['name']"
:width="240"
:side-name="'componentProp'"
:aside-position="'right'"
class="left-sidebar"
:slide-index="2"
:themes="'dark'"
:element="curComponent"
:view="canvasViewInfo[curComponent.id]"
:class="{ 'preview-aside': editMode === 'preview' }"
>
<component :is="findComponentAttr(curComponent)" />
</dv-sidebar>
<dv-sidebar
v-show="canvasPropertiesShow"
:title="t('visualization.screen_config')"
:width="240"
:side-name="'canvas'"
:aside-position="'right'"
class="left-sidebar"
:class="{ 'preview-aside': editMode === 'preview' }"
>
<canvas-attr></canvas-attr>
</dv-sidebar>
<div
v-show="viewsPropertiesShow"
style="height: 100%"
:class="{ 'preview-aside': editMode === 'preview' }"
>
<editor
:view="canvasViewInfo[curComponent ? curComponent.id : 'default']"
themes="dark"
:dataset-tree="state.datasetTree"
></editor>
</div>
</template>
<dv-sidebar
v-if="batchOptStatus"
:theme-info="'dark'"
:title="t('visualization.batch_style_set')"
:width="280"
aside-position="right"
class="left-sidebar"
:side-name="'batchOpt'"
>
<chart-style-batch-set themes="dark"></chart-style-batch-set>
</dv-sidebar>
</div>
</el-container>
</div>
<XpackComponent
jsname="L2NvbXBvbmVudC9lbWJlZGRlZC1pZnJhbWUvTmV3V2luZG93SGFuZGxlcg=="
@loaded="XpackLoaded"
@load-fail="XpackLoaded"
/>
<xpack-component jsname="L2NvbXBvbmVudC90aHJlc2hvbGQtd2FybmluZy9UaHJlc2hvbGREaWFsb2c=" />
<canvas-cache-dialog ref="canvasCacheOutRef" @doUseCache="doUseCache"></canvas-cache-dialog>
<dv-preview
v-if="fullscreenFlag"
style="z-index: 10"
ref="dvPreviewRef"
show-position="edit-preview"
:canvas-data-preview="componentData"
:canvas-style-preview="canvasStyleData"
:canvas-view-info-preview="canvasViewInfo"
:dv-info="{ ...dvInfo, status: 1 }"
></dv-preview>
<custom-tabs-sort ref="customTabsSortRef"></custom-tabs-sort>
</template>
<style lang="less">
.preview-layout-container {
}
.preview-content {
align-items: center;
}
.dv-common-layout {
height: 100vh;
width: 100vw;
overflow: hidden;
color: @dv-canvas-main-font-color;
.dv-layout-container {
height: calc(100vh - @top-bar-height - 1px);
.left-sidebar {
height: 100%;
}
.center {
display: flex;
flex-direction: column;
height: 100%;
flex: 1;
position: relative;
background-color: rgba(51, 51, 51, 1);
overflow: auto;
.content {
position: relative;
flex: 1;
width: 100%;
overflow: auto;
margin: auto;
}
}
.right-sidebar {
height: 100%;
}
}
&.dataease-w-h {
height: 100%;
width: 100%;
.dv-layout-container {
height: calc(100% - @top-bar-height);
}
}
}
.preview-aside {
width: 0px !important;
overflow: hidden;
padding: 0px;
}
.preview-aside-x {
height: 0px !important;
overflow: hidden;
padding: 0px;
}
.canvas-area-shadow {
box-sizing: border-box;
border: 1px solid rgba(85, 85, 85, 0.4);
}
.custom-dv-divider {
width: 100%;
height: 1px;
background: #000;
}
.canvas-dv-inner {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.de-ruler-icon-outer {
background: #2c2c2c;
position: absolute;
width: 30px;
height: 30px;
z-index: 3;
color: #ebebeb;
.de-ruler-icon {
margin-left: 6px;
margin-top: 6px;
font-size: 24px;
color: #ebebeb;
}
}
.canvas-drag {
position: absolute;
z-index: 1;
opacity: 0.3;
cursor: pointer;
width: 100%;
height: 100%;
}
.canvas-drag-tip {
position: absolute;
right: 5px;
bottom: -20px;
font-size: 12px;
color: rgb(169, 175, 184);
}
</style>