<template> <div ref="bs-render-wrap" :key="`${pageInfo.pageConfig.w}${pageInfo.pageConfig.h}`" class="bs-render-wrap design-drag-wrap render-theme-wrap" :style="{ width: pageInfo.pageConfig.w + 'px', height: pageInfo.pageConfig.h + 'px', backgroundColor:pageInfo.pageConfig.customTheme ==='light' ? pageInfo.pageConfig.lightBgColor:pageInfo.pageConfig.bgColor , backgroundImage:pageInfo.pageConfig.customTheme ==='light' ? `url(${this.getCoverPicture(pageInfo.pageConfig.lightBg)})`:`url(${this.getCoverPicture(pageInfo.pageConfig.bg)})` }" @drop="drop($event)" @dragover.prevent @click="handleClickOutside($event)" > <vdr v-for="chart in chartList" :id="chart.code" :key="chart.updateKey || chart.code" class="drag-item" :class="{ 'multiple-selected': activeCodes.includes(chart.code), }" :scale-ratio="scale" :x="chart.x" :y="chart.y" :w="chart.w" :h="chart.h" :min-width="10" :min-height="10" :draggable="!chart.locked" :resizable="!chart.locked" :parent="true" :debug="false" :is-conflict-check="false" :snap="true" :snap-tolerance="snapTolerance" :style="{ zIndex: chart.z || 0, }" :perspective="parseInt(`${chart.perspective == undefined ? 0 : chart.perspective}`)" :transform="`skew(${chart.skewX == undefined ? 0 : chart.skewX}deg, ${chart.skewY == undefined? 0 : chart.skewY}deg) rotateX(${chart.rotateX == undefined ? 0 : chart.rotateX}deg) rotateY(${chart.rotateY == undefined ? 0 : chart.rotateY}deg) rotateZ(${chart.rotateZ == undefined ? 0 : chart.rotateZ}deg)`" :grid="[1,1]" :handles="handlesList" class-name-handle="bs-handle-class" @activated="activated(...arguments, chart)" @dragging="onDrag(...arguments, chart)" @resizing="onResize(...arguments, chart)" @resizestop="resizestop(...arguments, chart)" @dragstop="dragstop(...arguments, chart)" @refLineParams="getRefLineParams" @mouseleave.native="resetPresetLineDelay" > <Configuration v-if="isInit" :config="chart" @openRightPanel="openRightPanel" @openDataViewDialog="openDataViewDialog" > <RenderCard :ref="'RenderCard' + chart.code" :config="chart" @styleHandler="styleHandler" /> </Configuration> </vdr> <span v-for="(vl, index) in vLine" v-show="vl.display" :key="index + 'vLine'" class="ref-line v-line" :style="{ left: vl.position, top: vl.origin, height: vl.lineLength }" /> <span v-for="(hl, index) in hLine" v-show="hl.display" :key="index + 'hLine'" class="ref-line h-line" :style="{ top: hl.position, left: hl.origin, width: hl.lineLength }" /> </div> </template> <script> import { mapState, mapMutations } from 'vuex' import RenderCard from './RenderCard.vue' import Configuration from './Configuration.vue' // import _ from 'lodash' import cloneDeep from 'lodash/cloneDeep' import vdr from '@gcpaas/vue-draggable-resizable-gorkys' import 'gc-vue-draggable-resizable/dist/VueDraggableResizable.css' import { randomString } from '../js/utils' import { compile } from 'tiny-sass-compiler/dist/tiny-sass-compiler.esm-browser.prod.js' import plotList, { getCustomPlots } from '../G2Plots/plotList' import { settingToTheme } from 'data-room-ui/js/utils/themeFormatting' import { getFileUrl } from 'data-room-ui/js/utils/file' export default { name: 'BigScreenRender', components: { RenderCard, Configuration, vdr }, props: { ruleKey: { type: Number, default: 0 } }, data () { return { handlesList: ['tl', 'tm', 'tr', 'mr', 'br', 'bm', 'bl', 'ml'], // 缩放手柄的数组 vLine: [], hLine: [], themeCss: '', // 临时冻结拖拽 freeze: false, plotList, rawChart: [] } }, computed: { ...mapState({ pageConfig: (state) => state.bigScreen.pageInfo.pageConfig, pageInfo: (state) => state.bigScreen.pageInfo, chartList: (state) => state.bigScreen.pageInfo.chartList, activeCode: (state) => state.bigScreen.activeCode, activeCodes: (state) => state.bigScreen.activeCodes, hoverCode: (state) => state.bigScreen.hoverCode, themeJson: (state) => state.bigScreen.pageInfo.pageConfig.themeJson, isInit: (state) => !state.bigScreen.pageLoading, scale: (state) => state.bigScreen.zoom / 100, snapTolerance: (state) => state.bigScreen.snapTolerance }) }, watch: { pageConfig: { handler (pageConfig) { this.$nextTick(() => { const style = document.createElement('style') if ( pageConfig && pageConfig.themeJson && pageConfig.themeJson.themeCss ) { const themeCss = pageConfig.themeJson.themeCss if (themeCss) { const themeStr = compile(themeCss).code style.type = 'text/css' style.innerText = themeStr document.getElementsByTagName('head')[0].appendChild(style) } } }) }, deep: true, immediate: true } }, mounted () { this.styleSet() this.plotList = [...this.plotList, ...getCustomPlots()] }, methods: { ...mapMutations('bigScreen', [ 'changeLayout', 'changeActiveCode', 'changeChartConfig', 'changeActiveItemConfig', 'changeActiveItemWH', 'addItem', 'delItem', 'resetPresetLine', 'changeGridShow', 'setPresetLine', 'saveTimeLine', 'changeActiveCodes' ]), // 判断鼠标点击的是画布中的高亮元素(被框选的)还是非高亮元素或者空白区域 // 如果是高亮元素则不会取消高亮状态,如果不是则取消高亮状态 handleClickOutside (event) { // 获取被点击的元素 const clickedElement = event.target const elementToHighlights = [] // 获取需要高亮的元素的引用 for (const code of this.activeCodes) { if (this.$refs['RenderCard' + code] && this.$refs['RenderCard' + code].length && this.$refs['RenderCard' + code][0]) { elementToHighlights.push(this.$refs['RenderCard' + code][0]) } } const isElementInHighlights = elementToHighlights.some((elementToHighlight) => { return elementToHighlight?.$el?.contains(clickedElement) }) if (!isElementInHighlights) { this.changeActiveCodes([]) } }, // 切换主题时针对远程组件触发样式修改的方法 styleHandler (config) { this.$nextTick(() => { this.$refs['RenderCard' + config.code][0]?.$refs[ config.code ]?.changeStyle(cloneDeep(config), true) }) }, // 获取到后端传来的主题样式并进行修改 styleSet () { const style = document.createElement('style') if (this.themeJson && this.themeJson.themeCss) { const styleStr = this.themeJson.themeCss const themeCss = compile(styleStr).code style.type = 'text/css' style.innerText = themeCss document.getElementsByTagName('head')[0].appendChild(style) } else { style.remove() } }, resetPresetLineDelay () { setTimeout(() => { this.resetPresetLine() }, 500) }, // 点击当前组件时打开右侧面板 openRightPanel (config) { this.$emit('openRightPanel', config) }, // 查看数据 openDataViewDialog (config) { this.$emit('openDataViewDialog', config) }, drop (e) { e.preventDefault() // 解决:火狐拖放后,总会默认打开百度搜索,如果是图片,则会打开图片的问题。 e.stopPropagation() const transferData = e.dataTransfer.getData('dragComponent') if (transferData) { this.addChart(transferData, { x: e?.x, y: e?.y }) } }, /** * 改变组件大小 * @param x * @param y * @param width * @param height * @param chart */ onResize (x, y, width, height, chart) { chart.x = x chart.y = y chart.w = width chart.h = height this.changeGridShow(true) this.setPresetLine({ ...chart }) }, /** * * @param x * @param y * @param chart */ onDrag (x, y, chart) { // 防止事件冒泡 event.stopPropagation() if (chart.group) { // 查找和自己是一个组合的组件 this.dragGroupChart(x, y, chart) } else { chart.x = x chart.y = y } this.changeGridShow(true) this.setPresetLine({ ...chart }) }, resizestop (left, top, width, height, chart) { this.changeChartConfig({ ...chart, w: width, h: height, x: left, y: top }) this.changeActiveItemConfig({ ...chart, w: width, h: height, x: left, y: top }) if (chart.code === this.activeCode) { this.changeActiveItemWH({ code: chart.code, w: width, h: height }) } this.saveTimeLine(`改变${chart?.title}大小`) this.changeGridShow(false) }, activated (chart) { this.rawChart = cloneDeep(chart) }, dragstop (left, top, chart) { if (!this.freeze) { if (this.rawChart.x !== left || this.rawChart.y !== top) { this.changeChartConfig({ ...chart, x: left, y: top }) this.changeActiveItemConfig({ ...chart, x: left, y: top }) if (chart.code === this.activeCode) { this.changeActiveItemWH({ code: chart.code, x: left, y: top }) } this.rawChart = cloneDeep(chart) } } else { const index = this.chartList.findIndex( (_chart) => _chart.code === chart.code ) this.$set(this.chartList, index, chart) this.changeChartConfig({ ...chart, updateKey: new Date().getTime() }) } this.changeGridShow(false) this.freeze = false this.saveTimeLine(`拖拽${chart?.title}`) }, // 辅助线 getRefLineParams (params) { const { vLine, hLine } = params this.vLine = vLine this.hLine = hLine }, // 新增元素 addChart (chart, position, isComponent) { const { left, top } = this.$el.getBoundingClientRect() const _chart = !chart.code ? JSON.parse(chart) : chart let option = _chart.option if (_chart.type === 'customComponent') { option = { ...this.plotList?.find((plot) => plot.name === _chart.name)?.option, theme: this.pageConfig.customTheme === 'dark' ? 'transparent' : 'light' } } const config = { ..._chart, x: parseInt(!chart.code ? (position.x - left - _chart.offsetX) / this.scale : position.x), y: parseInt(!chart.code ? (position.y - top - _chart.offsetX) / this.scale : position.y), width: 200 * this.scale, height: 200 * this.scale, code: !chart.code ? randomString(8) : chart.code, option } config.key = isComponent ? randomString(8) : config.code // isComponent = false 从左侧新增时需要初始化theme的内容 // isComponent = true从组件库添加自定义组件时不用初始化 if (!isComponent) { config.theme = settingToTheme(config, 'dark') config.theme = settingToTheme(config, 'light') } this.addItem(config) }, addSourceChart (chart, position) { const { left, top } = this.$el.getBoundingClientRect() const _chart = JSON.parse(chart) let option = _chart.option if (_chart.type === 'customComponent') { option = { ...this.plotList?.find((plot) => plot.name === _chart.name)?.option, theme: this.pageConfig.customTheme === 'dark' ? 'transparent' : 'light' } } const config = { ..._chart, x: parseInt((position.x - left) / this.scale), y: parseInt((position.y - top) / this.scale), width: 200 * this.scale, height: 200 * this.scale, code: randomString(8), option } config.key = config.code this.addItem(config) }, /** * 拖拽相同组合的组件 * @param x 组合元素当前x * @param y 组合元素当前y * @param chart */ dragGroupChart (x, y, chart) { if (chart.group) { const diffX = x - chart.x const diffY = y - chart.y const group = chart.group // 找到相同group的组件,并找到边界 const groupChartList = this.chartList.filter( (groupChart) => groupChart.group === group ) const groupMinX = Math.min( ...groupChartList?.map((groupChart) => groupChart.x + diffX) ) const groupMinY = Math.min( ...groupChartList?.map((groupChart) => groupChart.y + diffY) ) const groupMaxX = Math.max( ...groupChartList?.map( (groupChart) => groupChart.x + diffX + groupChart.w ) ) const groupMaxY = Math.max( ...groupChartList?.map( (groupChart) => groupChart.y + diffY + groupChart.h ) ) // 如果其中某个组件超出画布,则不移动 (此处无法阻止移动,故在拖拽结束后重置位置) if ( (groupMinX <= 0 || groupMinY <= 0 || groupMaxX >= this.pageConfig.w || groupMaxY >= this.pageConfig.h) && // 偏移的绝对值要大于0 (Math.abs(diffX) > 0 || Math.abs(diffY) > 0) ) { this.freeze = true return } // 移动相应的diff距离 groupChartList?.map((groupChart) => { this.changeChartConfig({ ...groupChart, x: groupChart.x + diffX, y: groupChart.y + diffY }) }) } }, /** * 获取图片访问地址,如果是相对路径则拼接上文件访问前缀地址 * @param url * @returns {*} */ getCoverPicture (url) { return getFileUrl(url) } } } </script> <style lang="scss" scoped> .bs-render-wrap { position: relative; background-size: cover; .drag-item { cursor: move; } ::v-deep .vdr { border: none; } .h-line { border-bottom: 1px dashed #0089d0; } .v-line { border-left: 1px dashed #0089d0; } .ref-line { background-color: transparent; } } .design-drag-wrap { box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); } .multiple-selected { border: 1px solid #fff !important; } //调整拖拽插件的句柄样式 //句柄公共样式 ::v-deep .bs-handle-class{ width: 16px!important; height: 16px!important; position: absolute; box-sizing: border-box; //background: #fff; border: 3px solid #c8ff00; } // 每个句柄不同样式 ::v-deep .bs-handle-class-tl{ top: -2px!important; left: -2px!important; display: block; cursor: nw-resize; border-right: none; border-bottom: none; } ::v-deep .bs-handle-class-tm{ top: -2px!important; left: calc(50% - 8px)!important; display: block; cursor: n-resize; border-left: none; border-right: none; border-bottom: none; } ::v-deep .bs-handle-class-tr{ top: -2px!important; right: -2px!important; display: block; cursor: ne-resize; border-left: none; border-bottom: none; } ::v-deep .bs-handle-class-mr{ top: calc(50% - 8px)!important; right: -2px!important; display: block; cursor: e-resize; border-left: none; border-top: none; border-bottom: none; } ::v-deep .bs-handle-class-br{ right: -2px!important; bottom: -2px!important; display: block; cursor: se-resize; border-left: none; border-top: none; } ::v-deep .bs-handle-class-bm{ right: calc(50% - 8px)!important; bottom: -2px!important; display: block; cursor: s-resize; border-left: none; border-right: none; border-top: none; } ::v-deep .bs-handle-class-bl{ left: -2px!important; bottom: -2px!important; display: block; cursor: sw-resize; border-right: none; border-top: none; } ::v-deep .bs-handle-class-ml{ top: calc(50% - 8px)!important; left: -2px!important; display: block; cursor: w-resize; border-top: none; border-right: none; border-bottom: none; } </style>