You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

580 lines
16 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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(${getCoverPicture(pageInfo.pageConfig.lightBg)})`:`url(${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'
import { customDeserialize } from 'data-room-ui/js/utils/jsonSerialize.js'
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 ? customDeserialize(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')
}
console.log('1', config)
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>