feat: 画布升级 增加矩阵 边界 对齐 吸附等功能

This commit is contained in:
wangjiahao 2021-06-10 17:12:31 +08:00
parent 704d86adf4
commit dba5eeb571
21 changed files with 1999 additions and 85 deletions

View File

@ -1,6 +1,8 @@
package io.dataease.controller.handler;
import io.dataease.controller.ResultHolder;
import io.dataease.controller.handler.annotation.I18n;
import io.dataease.i18n.Translator;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
@ -66,7 +68,8 @@ public class GlobalExceptionHandler implements ErrorController {
errorMessage = "The server responds " + code + " but no detailed message.";
}
}
return ResultHolder.error(errorMessage);
return ResultHolder.error(Translator.get(errorMessage));
}
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {

View File

@ -239,7 +239,7 @@ i18n_processing_data=Processing data now, Refresh later
i18n_union_already_exists=Union relation already exists
i18n_union_field_exists=The same field can't in two dataset
i18n_cron_time_error=Start time can't greater then end time
i18n_auth_source_be_canceled=This Auth Resource Already Be Canceled
i18n_auth_source_be_canceled=This Auth Resource Already Be Canceled,Please Connect Admin
i18n_username_exists=ID is already exists
i18n_ds_name_exists=Datasource name used
i18n_sync_job_exists=There is already a synchronization task running, please try again later

View File

@ -239,7 +239,7 @@ i18n_processing_data=正在处理数据,稍后刷新
i18n_union_already_exists=关联关系已存在
i18n_union_field_exists=两个数据集之间关联不能出现多次相同字段
i18n_cron_time_error=开始时间不能大于结束时间
i18n_auth_source_be_canceled=当前资源授权权限已经被取消
i18n_auth_source_be_canceled=本用户当前资源所有授权权限已经被取消,如需再次开通,请联系管理员
i18n_username_exists=用户 ID 已存在
i18n_ds_name_exists=数据源名称已被使用
i18n_sync_job_exists=已经有同步任务在运行,稍后重试

View File

@ -241,7 +241,7 @@ i18n_processing_data=正在處理數據,稍後刷新
i18n_union_already_exists=關聯關系已存在
i18n_union_field_exists=兩個數據集之間關聯不能出現多次相同字段
i18n_cron_time_error=開始時間不能大於結束時間
i18n_auth_source_be_canceled=當前資源授權權限已經被取消
i18n_auth_source_be_canceled=本用户当前资源所有授权权限已经被取消,如需再次开通,请联系管理员
i18n_username_exists=用戶ID已存在
i18n_ds_name_exists=數據源名稱已被使用
i18n_sync_job_exists=已經有同步任務在運行,稍後重試

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
export const events = {
mouse: {
start: 'mousedown',
move: 'mousemove',
stop: 'mouseup'
},
touch: {
start: 'touchstart',
move: 'touchmove',
stop: 'touchend'
}
}
// 禁止用户选取
export const userSelectNone = {
userSelect: 'none',
MozUserSelect: 'none',
WebkitUserSelect: 'none',
MsUserSelect: 'none'
}
// 用户选中自动
export const userSelectAuto = {
userSelect: 'auto',
MozUserSelect: 'auto',
WebkitUserSelect: 'auto',
MsUserSelect: 'auto'
}

View File

@ -1,18 +1,18 @@
<template>
<svg class="grid" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="smallGrid" width="7.236328125" height="7.236328125" patternUnits="userSpaceOnUse">
<!-- <pattern id="smallGrid" width="5" height="5" patternUnits="userSpaceOnUse">-->
<!-- <path-->
<!-- d="M 5 0 L 0 0 0 5"-->
<!-- fill="none"-->
<!-- stroke="rgba(207, 207, 207, 0.3)"-->
<!-- stroke-width="1"-->
<!-- />-->
<!-- </pattern>-->
<pattern id="grid" :width="matrixStyle.width" :height="matrixStyle.height" patternUnits="userSpaceOnUse">
<rect :width="matrixStyle.width" :height="matrixStyle.height" fill="url(#smallGrid)" />
<path
d="M 7.236328125 0 L 0 0 0 7.236328125"
fill="none"
stroke="rgba(207, 207, 207, 0.3)"
stroke-width="1"
/>
</pattern>
<pattern id="grid" width="36.181640625" height="36.181640625" patternUnits="userSpaceOnUse">
<rect width="36.181640625" height="36.181640625" fill="url(#smallGrid)" />
<path
d="M 36.181640625 0 L 0 0 0 36.181640625"
:d="pathD"
fill="none"
stroke="rgba(186, 186, 186, 0.5)"
stroke-width="1"
@ -23,6 +23,28 @@
</svg>
</template>
<script>
export default {
props: {
matrixStyle: {
type: Object
}
},
data() {
return {
}
},
computed: {
pathD: function() {
return 'M ' + this.matrixStyle.width + ' 0 L 0 0 0 ' + this.matrixStyle.height
}
}
}
</script>
<style lang="scss" scoped>
.grid {
position: absolute;

View File

@ -1,5 +1,6 @@
<template>
<div
v-if="showDrag"
id="editor"
class="editor"
:class="{ edit: isEdit }"
@ -8,18 +9,27 @@
@mousedown="handleMouseDown"
>
<!-- 网格线 -->
<Grid />
<Grid v-if="canvasStyleData.auxiliaryMatrix" :matrix-style="matrixStyle" />
<!--页面组件列表展示-->
<Shape
<de-drag
v-for="(item, index) in componentData"
:key="item.id"
:index="index"
:x="getShapeStyleIntDeDrag(item.style,'left')"
:y="getShapeStyleIntDeDrag(item.style,'top')"
:w="getShapeStyleIntDeDrag(item.style,'width')"
:h="getShapeStyleIntDeDrag(item.style,'height')"
:r="item.style.rotate"
:parent="true"
:rotatable="rotatable"
:default-style="getShapeStyleInt(item.style)"
:style="getShapeStyle(item.style)"
:active="item === curComponent"
:element="item"
:index="index"
:class="{ lock: item.isLock }"
class-name-active="de-drag-active"
:class="{'gap_class':canvasStyleData.panel.gap==='yes'}"
:snap="true"
:snap-tolerance="5"
@refLineParams="getRefLineParams"
>
<component
:is="item.component"
@ -63,19 +73,33 @@
:element="item"
@input="handleInput"
/> -->
</Shape>
</de-drag>
<!-- 右击菜单 -->
<ContextMenu />
<!-- 标线 (临时去掉标线 吸附等功能)-->
<!-- <MarkLine />-->
<!-- 选中区域 -->
<Area v-show="isShowArea" :start="start" :width="width" :height="height" />
<!-- <Area v-show="isShowArea" :start="start" :width="width" :height="height" />-->
<span
v-for="(item, index) in vLine"
v-show="item.display"
:key="'v_'+index"
class="ref-line v-line"
:style="{
left: item.position,
top: item.origin,
height: item.lineLength,
}"
/>
</div>
</template>
<script>
import { mapState } from 'vuex'
import Shape from './Shape'
import DeDrag from '@/components/DeDrag'
// eslint-disable-next-line no-unused-vars
import { getStyle, getComponentRotatedStyle } from '@/components/canvas/utils/style'
import { $ } from '@/components/canvas/utils/utils'
@ -89,7 +113,7 @@ import { Condition } from '@/components/widget/bean/Condition'
import bus from '@/utils/bus'
export default {
components: { Shape, ContextMenu, MarkLine, Area, Grid },
components: { Shape, ContextMenu, MarkLine, Area, Grid, DeDrag },
props: {
isEdit: {
type: Boolean,
@ -128,7 +152,23 @@ export default {
needToChangeWidth: [
'left',
'width'
]
],
// private
rotatable: false,
//
matrixStyle: {
width: 80,
height: 20
},
// 12 * 24
matrixCount: {
x: 12,
y: 24
},
customStyleHistory: null,
showDrag: true,
vLine: [],
hLine: []
}
},
watch: {
@ -146,7 +186,6 @@ export default {
}
},
computed: {
customStyle() {
let style = {
width: this.format(this.canvasStyleData.width, this.scaleWidth) + 'px',
@ -327,19 +366,26 @@ export default {
handleContextMenu(e) {
e.stopPropagation()
e.preventDefault()
//
let target = e.target
let top = e.offsetY
let left = e.offsetX
while (target instanceof SVGElement) {
target = target.parentNode
}
let top = 0
let left = 0
//
if (this.curComponent && !target.className.includes('editor')) {
top = this.curComponent.style.top * this.scaleHeight / 100 + e.offsetY
left = this.curComponent.style.left * this.scaleWidth / 100 + e.offsetX
} else {
//
top = e.offsetY
left = e.offsetX
while (!target.className.includes('editor')) {
left += target.offsetLeft
top += target.offsetTop
target = target.parentNode
while (!target.className.includes('editor')) {
left += target.offsetLeft
top += target.offsetTop
target = target.parentNode
}
}
this.$store.commit('showContextMenu', { top, left })
@ -443,11 +489,62 @@ export default {
}
},
changeScale() {
//
const style = {
width: this.format(this.canvasStyleData.width, this.scaleWidth) + 'px',
height: this.format(this.canvasStyleData.height, this.scaleHeight) + 'px'
}
if (this.customStyleHistory && this.customStyleHistory !== style) {
this.showDrag = false
this.$nextTick(() => (this.showDrag = true))
}
this.customStyleHistory = style
if (this.canvasStyleData.matrixCount) {
this.matrixCount = this.canvasStyleData.matrixCount
}
if (this.outStyle.width && this.outStyle.height) {
//
if (!this.canvasStyleData.selfAdaption) {
this.matrixStyle.width = this.canvasStyleData.width / this.matrixCount.x
this.matrixStyle.height = this.canvasStyleData.height / this.matrixCount.y
} else {
this.matrixStyle.width = this.outStyle.width / this.matrixCount.x
this.matrixStyle.height = this.outStyle.height / this.matrixCount.y
}
this.scaleWidth = parseInt(this.outStyle.width * 100 / this.canvasStyleData.width)
this.scaleHeight = parseInt(this.outStyle.height * 100 / this.canvasStyleData.height)
this.$store.commit('setCurCanvasScale', { scaleWidth: this.scaleWidth, scaleHeight: this.scaleHeight })
this.$store.commit('setCurCanvasScale',
{
scaleWidth: this.scaleWidth,
scaleHeight: this.scaleHeight,
matrixStyleWidth: this.matrixStyle.width,
matrixStyleHeight: this.matrixStyle.height
})
}
},
getShapeStyleIntDeDrag(style, prop) {
if (prop === 'rotate') {
return style['rotate']
}
if (prop === 'width') {
return this.format(style['width'], this.scaleWidth)
}
if (prop === 'left') {
return this.format(style['left'], this.scaleWidth)
}
if (prop === 'height') {
return this.format(style['height'], this.scaleHeight)
}
if (prop === 'top') {
return this.format(style['top'], this.scaleHeight)
}
},
getRefLineParams(params) {
console.log(params)
const { vLine, hLine } = params
this.vLine = vLine
this.hLine = hLine
}
}
}
@ -456,7 +553,7 @@ export default {
<style lang="scss" scoped>
.editor {
position: relative;
background: #fff;
/*background: #fff;*/
margin: auto;
background-size:100% 100% !important;
@ -465,10 +562,35 @@ export default {
}
}
.edit {
outline: 1px solid gainsboro;
.component {
outline: none;
width: 100%;
height: 100%;
}
}
.gap_class{
padding:3px;
}
//
.de-drag-active{
outline: 1px solid #70c0ff;
user-select: none;
}
.ref-line {
position: absolute;
background-color: #70c0ff;;
z-index: 9999;
}
.v-line {
width: 1px;
}
.h-line {
height: 1px;
}
</style>

View File

@ -2,6 +2,11 @@
<div>
<div class="toolbar">
<div class="canvas-config" style="margin-right: 10px">
<el-switch v-model="canvasStyleData.auxiliaryMatrix" :width="35" label="矩阵设计" name="auxiliaryMatrix" />
<span>矩阵设计</span>
</div>
<div class="canvas-config" style="margin-right: 10px">
<el-switch v-model="canvasStyleData.selfAdaption" :width="35" label="自适应画布区域" name="selfAdaption" />
<span>自适应画布区域 </span>

View File

@ -2,41 +2,41 @@ import { swap } from '@/components/canvas/utils/utils'
import toast from '@/components/canvas/utils/toast'
export default {
mutations: {
upComponent({ componentData, curComponentIndex }) {
// 上移图层 index表示元素在数组中越往后
if (curComponentIndex < componentData.length - 1) {
swap(componentData, curComponentIndex, curComponentIndex + 1)
} else {
toast('已经到顶了')
}
},
downComponent({ componentData, curComponentIndex }) {
// 下移图层 index表示元素在数组中越往前
if (curComponentIndex > 0) {
swap(componentData, curComponentIndex, curComponentIndex - 1)
} else {
toast('已经到底了')
}
},
topComponent({ componentData, curComponentIndex }) {
// 置顶
if (curComponentIndex < componentData.length - 1) {
swap(componentData, curComponentIndex, componentData.length - 1)
} else {
toast('已经到顶了')
}
},
bottomComponent({ componentData, curComponentIndex }) {
// 置底
if (curComponentIndex > 0) {
swap(componentData, curComponentIndex, 0)
} else {
toast('已经到底了')
}
},
mutations: {
upComponent({ componentData, curComponentIndex }) {
// 上移图层 index表示元素在数组中越往后
if (curComponentIndex < componentData.length - 1) {
swap(componentData, curComponentIndex, curComponentIndex + 1)
} else {
toast('已经到顶了')
}
},
downComponent({ componentData, curComponentIndex }) {
// 下移图层 index表示元素在数组中越往前
if (curComponentIndex > 0) {
swap(componentData, curComponentIndex, curComponentIndex - 1)
} else {
toast('已经到底了')
}
},
topComponent({ componentData, curComponentIndex }) {
// 置顶
if (curComponentIndex < componentData.length - 1) {
swap(componentData, curComponentIndex, componentData.length - 1)
} else {
toast('已经到顶了')
}
},
bottomComponent({ componentData, curComponentIndex }) {
// 置底
if (curComponentIndex > 0) {
swap(componentData, curComponentIndex, 0)
} else {
toast('已经到底了')
}
}
}
}

View File

@ -25,6 +25,7 @@ export default {
},
recordSnapshot(state) {
console.log('recordSnapshot')
// 添加新的快照
state.snapshotData[++state.snapshotIndex] = deepCopy(state.componentData)
state.snapshotStyleData[state.snapshotIndex] = deepCopy(state.canvasStyleData)

View File

@ -952,6 +952,7 @@ export default {
close_aided_design: 'Close Component Aided Design',
open_style_design: 'Open Style Design',
close_style_design: 'Close Style Design',
matrix_design: 'Matrix Design',
left: 'X-Axis',
top: 'Y-Axis',
height: 'Height',

View File

@ -952,6 +952,7 @@ export default {
close_aided_design: '关闭组件辅助设计',
open_style_design: '打开样式设计',
close_style_design: '关闭样式设计',
matrix_design: '矩阵设计',
left: 'x 坐标',
top: 'y 坐标',
height: '高',

View File

@ -952,6 +952,7 @@ export default {
close_aided_design: '关闭组件辅助设计',
open_style_design: '打开样式设计',
close_style_design: '关闭样式设计',
matrix_design: '矩阵设计',
left: 'x 坐标',
top: 'y 坐标',
height: '高',

View File

@ -27,6 +27,7 @@ Vue.use(VueClipboard)
Vue.use(widgets)
Vue.prototype.$api = api
import * as echarts from 'echarts'
Vue.prototype.$echarts = echarts

View File

@ -72,7 +72,7 @@ const data = {
},
setCurComponent(state, { component, index }) {
// console.log('curComponent' + JSON.stringify(component))
console.log('curComponent' + JSON.stringify(component))
state.curComponent = component
state.curComponentIndex = index
},
@ -82,12 +82,12 @@ const data = {
},
setShapeStyle({ curComponent, canvasStyleData, curCanvasScale }, { top, left, width, height, rotate }) {
if (top) curComponent.style.top = parseInt(canvasStyleData.selfAdaption ? (top * 100 / curCanvasScale.scaleHeight) : top)
if (left) curComponent.style.left = parseInt(canvasStyleData.selfAdaption ? (left * 100 / curCanvasScale.scaleWidth) : left)
if (width) curComponent.style.width = parseInt(canvasStyleData.selfAdaption ? (width * 100 / curCanvasScale.scaleWidth) : width)
if (height) curComponent.style.height = parseInt(canvasStyleData.selfAdaption ? (height * 100 / curCanvasScale.scaleHeight) : height)
if (rotate) curComponent.style.rotate = rotate
// console.log('setShapeStyle' + JSON.stringify(curComponent))
if (top || top === 0) curComponent.style.top = parseInt(canvasStyleData.selfAdaption ? (top * 100 / curCanvasScale.scaleHeight) : top)
if (left || left === 0) curComponent.style.left = parseInt(canvasStyleData.selfAdaption ? (left * 100 / curCanvasScale.scaleWidth) : left)
if (width || width === 0) curComponent.style.width = parseInt(canvasStyleData.selfAdaption ? (width * 100 / curCanvasScale.scaleWidth) : width)
if (height || height === 0) curComponent.style.height = parseInt(canvasStyleData.selfAdaption ? (height * 100 / curCanvasScale.scaleHeight) : height)
if (rotate || rotate === 0) curComponent.style.rotate = rotate
// console.log('setShapeStyle:curComponent' + 'top:' + top + ';left:' + left + '====' + JSON.stringify(curComponent))
},
setShapeSingleStyle({ curComponent }, { key, value }) {

60
frontend/src/utils/dom.js Normal file
View File

@ -0,0 +1,60 @@
import { isFunction } from './fns'
// 将选择器与父元素匹配
export function matchesSelectorToParentElements(el, selector, baseNode) {
let node = el
const matchesSelectorFunc = [
'matches',
'webkitMatchesSelector',
'mozMatchesSelector',
'msMatchesSelector',
'oMatchesSelector'
].find(func => isFunction(node[func]))
if (!isFunction(node[matchesSelectorFunc])) return false
do {
if (node[matchesSelectorFunc](selector)) return true
if (node === baseNode) return false
node = node.parentNode
} while (node)
return false
}
export function getComputedSize($el) {
const style = window.getComputedStyle($el)
return [
parseFloat(style.getPropertyValue('width'), 10),
parseFloat(style.getPropertyValue('height'), 10)
]
}
// 添加事件
export function addEvent(el, event, handler) {
if (!el) {
return
}
if (el.attachEvent) {
el.attachEvent('on' + event, handler)
} else if (el.addEventListener) {
el.addEventListener(event, handler, true)
} else {
el['on' + event] = handler
}
}
// 删除事件
export function removeEvent(el, event, handler) {
if (!el) {
return
}
if (el.detachEvent) {
el.detachEvent('on' + event, handler)
} else if (el.removeEventListener) {
el.removeEventListener(event, handler, true)
} else {
el['on' + event] = null
}
}

60
frontend/src/utils/fns.js Normal file
View File

@ -0,0 +1,60 @@
export function isFunction(func) {
return (typeof func === 'function' || Object.prototype.toString.call(func) === '[object Function]')
}
// 对齐栅格
export function snapToGrid(grid, pendingX, pendingY, scale = 1) {
const x = Math.round((pendingX / scale) / grid[0]) * grid[0]
const y = Math.round((pendingY / scale) / grid[1]) * grid[1]
return [x, y]
}
// 获取rect模型
export function getSize(el) {
const rect = el.getBoundingClientRect()
return [
parseInt(rect.width),
parseInt(rect.height)
]
}
export function computeWidth(parentWidth, left, right) {
return parentWidth - left - right
}
export function computeHeight(parentHeight, top, bottom) {
return parentHeight - top - bottom
}
export function restrictToBounds(value, min, max) {
if (min !== null && value < min) {
return min
}
if (max !== null && max < value) {
return max
}
return value
}
// 返回相对于参考点旋转后的坐标
export function rotatedPoint(originX, originY, offsetX, offsetY, rotate) {
const rad = (Math.PI / 180) * rotate
const cos = Math.cos(rad)
const sin = Math.sin(rad)
const x = offsetX - originX
const y = offsetY - originY
return {
x: x * cos - y * sin + originX,
y: x * sin + y * cos + originY
}
}
// 根据相对坐标返回角度,正方形为顺时针
export function getAngle(x, y) {
let theta = Math.atan2(y, x) // 正切转弧度
theta = Math.round((180 / Math.PI) * theta) // 弧度转角度
if (theta < 0) theta = 360 + theta // 控制角度在0~360度
return theta // 返回角度
}

View File

@ -7,8 +7,8 @@
trigger="click"
>
<el-col>
<el-radio v-model="panelStyleForm.gap" label="yes" @change="onChangePanelStyle">{{ $t('panel.gap') }} </el-radio>
<el-radio v-model="panelStyleForm.gap" label="no" @change="onChangePanelStyle">{{ $t('panel.no_gap') }}</el-radio>
<el-radio v-model="panel.gap" label="yes" @change="onChangePanelStyle">{{ $t('panel.gap') }} </el-radio>
<el-radio v-model="panel.gap" label="no" @change="onChangePanelStyle">{{ $t('panel.no_gap') }}</el-radio>
</el-col>
<el-button slot="reference" size="mini" class="shape-item">{{ $t('panel.component_gap') }} <i class="el-icon-setting el-icon--right" /></el-button>
</el-popover>
@ -18,18 +18,32 @@
<script>
import { DEFAULT_PANEL_STYLE } from '@/views/panel/panel'
import { mapState } from 'vuex'
import { deepCopy } from '@/components/canvas/utils/utils'
export default {
name: 'BackgroundSelector',
props: {
},
data() {
return {
panelStyleForm: JSON.parse(JSON.stringify(DEFAULT_PANEL_STYLE))
panel: null
}
},
computed: mapState([
'canvasStyleData'
]),
created() {
//
this.panel = this.canvasStyleData.panel
},
methods: {
onChangePanelStyle() {
this.$emit('onChangePanelStyle', this.panelStyleForm)
const canvasStyleData = deepCopy(this.canvasStyleData)
canvasStyleData.panel = this.panel
this.$store.commit('setCanvasStyle', canvasStyleData)
this.$store.commit('recordSnapshot')
}
}
}

View File

@ -11,7 +11,7 @@
<el-collapse-item :title="$t('panel.panel')" name="panel">
<el-row style="background-color: #f7f8fa; margin: 5px">
<background-selector class="attr-selector" />
<!-- <component-gap class="attr-selector" />-->
<component-gap class="attr-selector" />
</el-row>
</el-collapse-item>
<el-collapse-item :title="$t('chart.module_style')" name="component">

View File

@ -74,7 +74,7 @@
</de-aside-container>
<!--画布区域-->
<de-main-container id="canvasInfo-main" style="margin-left: 5px;margin-right: 5px">
<de-main-container id="canvasInfo-main">
<!--左侧抽屉-->
<el-drawer
:visible.sync="show"